232 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};