232 lines
12 KiB
TypeScript
232 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||
import { ChevronRight, BarChart3, Plus, AtSign, Send, PieChart, Globe, TrendingUp } from 'lucide-react';
|
||
import { StockChart } from '../components/StockChart';
|
||
import { STOCK_DATA_CONFIG } from '../data/mockData';
|
||
|
||
// 已移至 data/mockData.ts 作为共享数据
|
||
|
||
// 研判结论函数
|
||
const getJudgmentConclusion = (dimension: string, tab: string): string => {
|
||
const conclusions: Record<string, Record<string, string>> = {
|
||
valuation: {
|
||
'PB 质量与对比': '茅台PB处于历史低位,质量溢价收敛,估值安全边际显著',
|
||
'PE 质量与对比': '茅台PE估值回落至合理区间,盈利质量稳健支撑估值底部',
|
||
'PS 质量与对比': '市销率处于历史中枢偏下,收入增长预期稳定',
|
||
'股息回报': '股息率提升至历史高位,分红回报吸引力凸显'
|
||
},
|
||
macro: {
|
||
'ERP 风险溢价': 'ERP位于历史高位,权益资产性价比凸显',
|
||
'无风险利率': '无风险利率处于下行通道,利好权益资产估值修复',
|
||
'信用利差': '信用利差收窄,市场风险偏好改善,流动性环境友好'
|
||
},
|
||
assets: {
|
||
'同行业主要公司': '茅台相对同行业走势强劲,行业龙头地位稳固',
|
||
'上下游主要公司': '产业链整体景气度提升,上下游传导效应积极',
|
||
'同类型风格公司': '消费龙头风格轮动回归,高端品牌溢价重估'
|
||
},
|
||
capital: {
|
||
'市场成交活跃度': '成交量温和放大,市场情绪逐步回暖',
|
||
'主力资金流向': '主力资金持续净流入,机构配置意愿增强',
|
||
'北向资金持仓': '北向资金持续增持,外资看好长期投资价值'
|
||
}
|
||
};
|
||
|
||
return conclusions[dimension]?.[tab] || '当前指标处于合理区间,关注后续走势变化';
|
||
};
|
||
|
||
// 图表解释函数
|
||
const getChartExplanation = (dimension: string, tab: string): string => {
|
||
const explanations: Record<string, Record<string, string>> = {
|
||
valuation: {
|
||
'PB 质量与对比': '该图展示贵州茅台市净率(PB)的历史走势。PB值越低,说明相对净资产的估值越便宜。曲线下降表示估值压缩,上升则表示估值扩张。',
|
||
'PE 质量与对比': '该图反映市盈率(PE)变化趋势。PE值越低,说明股价相对盈利能力越便宜。可观察估值是否处于历史低位,判断投资价值。',
|
||
'PS 质量与对比': '该图显示市销率(PS)的历史波动。PS衡量股价相对营收的估值水平,低位通常意味着被低估的机会。',
|
||
'股息回报': '该图展示股息率的历史变化。股息率越高,持有股票的分红回报越好,是价值投资者关注的重要指标。'
|
||
},
|
||
macro: {
|
||
'ERP 风险溢价': '该图展示股权风险溢价(ERP)走势。ERP处于高位时,权益资产相对债券更具吸引力,是配置股票的良好时机。',
|
||
'无风险利率': '该图反映无风险利率(如国债收益率)的变化。利率下行通常利好股票估值,尤其是核心资产的长期价值。',
|
||
'信用利差': '该图显示信用利差的历史波动。利差扩大表示市场风险偏好下降,利差收窄则表示风险偏好回升。'
|
||
},
|
||
assets: {
|
||
'同行业主要公司': '该图对比茅台与同行业龙头的走势。可观察茅台相对行业的强弱,判断其竞争地位变化。',
|
||
'上下游主要公司': '该图展示茅台与产业链相关公司的走势对比。可从中洞察产业链景气度及传导效应。',
|
||
'同类型风格公司': '该图对比具有相似特征(如消费、高端品牌)的公司走势,帮助识别风格轮动和板块机会。'
|
||
},
|
||
capital: {
|
||
'市场成交活跃度': '该图反映市场成交量的变化。成交量萎缩通常意味着观望情绪浓厚,放量则表示资金入场或离场信号。',
|
||
'主力资金流向': '该图追踪主力资金的净流入流出。持续流入表示机构看好,流出则可能暗示减仓或调仓。',
|
||
'北向资金持仓': '该图显示外资通过陆股通的持仓变化。北向资金增持通常被视为积极信号,减持则需关注原因。'
|
||
}
|
||
};
|
||
|
||
return explanations[dimension]?.[tab] || '该图展示了相关指标的历史走势和变化趋势,可用于分析当前所处位置及未来可能的方向。';
|
||
};
|
||
|
||
const DIMENSIONS_CONFIG = {
|
||
valuation: {
|
||
label: '估值逻辑',
|
||
icon: <BarChart3 size={24} />,
|
||
tabs: STOCK_DATA_CONFIG.valuation.items.map(item => item.label),
|
||
data: STOCK_DATA_CONFIG.valuation.items,
|
||
color: '#882323'
|
||
},
|
||
capital: {
|
||
label: '资金流向',
|
||
icon: <PieChart size={24} />,
|
||
tabs: STOCK_DATA_CONFIG.capital.items.map(item => item.label),
|
||
data: STOCK_DATA_CONFIG.capital.items,
|
||
color: '#882323'
|
||
},
|
||
macro: {
|
||
label: '宏观胜率背景',
|
||
icon: <Globe size={24} />,
|
||
tabs: STOCK_DATA_CONFIG.macro.items.map(item => item.label),
|
||
data: STOCK_DATA_CONFIG.macro.items,
|
||
color: '#882323'
|
||
},
|
||
assets: {
|
||
label: '相关资产走势',
|
||
icon: <TrendingUp size={24} />,
|
||
tabs: STOCK_DATA_CONFIG.assets.items.map(item => item.label),
|
||
data: STOCK_DATA_CONFIG.assets.items,
|
||
color: '#882323'
|
||
}
|
||
};
|
||
|
||
export const DetailPage: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
|
||
// Get dimension from URL or default to 'valuation'
|
||
const dimensionKey = searchParams.get('tab') || 'valuation';
|
||
const isValidDimension = Object.keys(DIMENSIONS_CONFIG).includes(dimensionKey);
|
||
const activeDimensionKey = isValidDimension ? dimensionKey : 'valuation';
|
||
|
||
const currentConfig = DIMENSIONS_CONFIG[activeDimensionKey as keyof typeof DIMENSIONS_CONFIG];
|
||
|
||
const [activeSubTab, setActiveSubTab] = useState(currentConfig.tabs[0]);
|
||
|
||
// 获取当前选中tab对应的数据
|
||
const currentTabData = currentConfig.data.find(item => item.label === activeSubTab)?.chartData || currentConfig.data[0]?.chartData || [];
|
||
|
||
// When dimension changes (URL changes), reset sub-tab to the first one of that dimension
|
||
useEffect(() => {
|
||
setActiveSubTab(currentConfig.tabs[0]);
|
||
}, [activeDimensionKey]);
|
||
|
||
return (
|
||
<div className="flex min-h-screen bg-slate-50 text-slate-900 overflow-hidden">
|
||
|
||
<div className="flex-1 flex flex-col h-screen min-w-0">
|
||
{/* Header */}
|
||
<header className="h-14 bg-white border-b border-slate-200 px-6 flex items-center justify-between shrink-0">
|
||
<div className="flex items-center gap-4 text-sm text-slate-500">
|
||
<span onClick={() => navigate('/dashboard')} className="cursor-pointer hover:text-blue-600 transition-colors">股票详情</span>
|
||
<ChevronRight size={14} />
|
||
<span className="text-slate-900 font-medium">贵州茅台</span>
|
||
<span className="bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded text-xs font-mono">600519.SH</span>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-6">
|
||
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content Area */}
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
<div className="max-w-[1000px] mx-auto">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="text-slate-900">
|
||
{currentConfig.icon}
|
||
</div>
|
||
<h1 className="text-xl font-bold">{currentConfig.label}</h1>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="flex items-center gap-8 border-b border-slate-200 mb-4 overflow-x-auto no-scrollbar">
|
||
{currentConfig.tabs.map((tab) => {
|
||
const isActive = tab === activeSubTab;
|
||
return (
|
||
<button
|
||
key={tab}
|
||
onClick={() => setActiveSubTab(tab)}
|
||
className={`pb-3 text-sm font-medium whitespace-nowrap transition-colors border-b-2 ${
|
||
isActive
|
||
? 'border-blue-600 text-blue-600'
|
||
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
||
}`}
|
||
>
|
||
{tab}
|
||
</button>
|
||
)
|
||
})}
|
||
<button className="pb-3 text-slate-400 hover:text-slate-600"><Plus size={18} /></button>
|
||
</div>
|
||
|
||
{/* Chart Card */}
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-5 mb-4">
|
||
<div className="flex flex-col lg:flex-row gap-8">
|
||
{/* Chart Area */}
|
||
<div className="flex-1 min-h-[300px]">
|
||
<div className="h-[320px] w-full">
|
||
<StockChart
|
||
data={currentTabData}
|
||
color="#882323"
|
||
height="100%"
|
||
legend={`贵州茅台_${activeSubTab.split(' ')[0]}`}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Panel: Conclusion & Chart Explanation */}
|
||
<div className="w-full lg:w-72 flex flex-col gap-4">
|
||
{/* 研判结论 */}
|
||
<div>
|
||
<h3 className="text-xs font-semibold text-slate-500 mb-2">研判结论</h3>
|
||
<div className="bg-blue-50 rounded-xl p-3 border border-blue-100">
|
||
<p className="text-sm text-slate-800 font-medium leading-relaxed text-center">
|
||
{getJudgmentConclusion(activeDimensionKey, activeSubTab)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 图解释 */}
|
||
<div>
|
||
<h3 className="text-xs font-semibold text-slate-500 mb-2">图解释</h3>
|
||
<div className="bg-blue-50 rounded-xl p-3 border border-blue-100">
|
||
<p className="text-sm text-slate-800 font-medium leading-relaxed">
|
||
{getChartExplanation(activeDimensionKey, activeSubTab)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI Modification Prompt */}
|
||
<div className="max-w-4xl mx-auto mt-6 mb-8">
|
||
<div className="bg-white rounded-xl shadow-lg border border-slate-200 p-2 focus-within:ring-2 focus-within:ring-blue-500/20 focus-within:border-blue-500 transition-all">
|
||
<textarea
|
||
className="w-full p-3 bg-transparent border-none outline-none resize-none text-sm min-h-[60px]"
|
||
placeholder="请输入修改内容...例如:'帮我把时间范围改成过去5年' 或者 '添加一个PE对比图表'"
|
||
></textarea>
|
||
<div className="flex items-center justify-between px-1 pb-1">
|
||
<div className="flex items-center gap-1">
|
||
<button className="p-1.5 text-slate-400 hover:bg-slate-50 rounded-full transition-colors"><Plus size={18} /></button>
|
||
<button className="p-1.5 text-slate-400 hover:bg-slate-50 rounded-full transition-colors"><AtSign size={18} /></button>
|
||
</div>
|
||
<button className="w-8 h-8 bg-indigo-500 hover:bg-indigo-600 text-white rounded-full flex items-center justify-center shadow-md transition-colors">
|
||
<Send size={16} className="-ml-0.5 mt-0.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}; |