import React, { useState, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { ChevronRight, ChevronDown, BarChart3, PieChart, Globe, TrendingUp, LayoutTemplate, Save, FileText, ArrowDownRight, GripVertical, X, ChevronsLeft, ArrowUpRight, Pencil, Settings, Star, Plus } from 'lucide-react'; import { StockChart } from '../components/StockChart'; import { STOCK_DATA_CONFIG } from '../data/mockData'; // --- Mock Data Generators --- // 已移至 data/mockData.ts,这里保留引用 // Define initial data with Icons attached const INITIAL_DATA = [ { ...STOCK_DATA_CONFIG.valuation, icon: , tags: ['A股市场', '估值'], description: '从估值水平、历史分位、质量溢价等多角度评估当前估值合理性', }, { ...STOCK_DATA_CONFIG.macro, icon: , tags: ['A股市场', '宏观'], description: '结合宏观经济环境、无风险利率、风险溢价等因素分析投资胜率', }, { ...STOCK_DATA_CONFIG.assets, icon: , tags: ['A股市场', '基本面'], description: '跟踪产业链上下游走势、竞争格局变化等基本面相关资产表现', }, { ...STOCK_DATA_CONFIG.capital, icon: , tags: ['A股市场', '资金面'], description: '观察市场资金流向、成交活跃度等资金面动态变化', } ]; // 格式化摘要文本,添加标签高亮 const formatSummary = (text: string): string => { const keywords: Record = { '质量领跑': { color: 'text-emerald-800', bg: 'bg-emerald-100' }, '估值下行': { color: 'text-red-800', bg: 'bg-red-100' }, '历史低位': { color: 'text-emerald-800', bg: 'bg-emerald-100' }, '估值质量高': { color: 'text-blue-800', bg: 'bg-blue-100' }, '历史高位': { color: 'text-red-800', bg: 'bg-red-100' }, '历史极低位': { color: 'text-gray-800', bg: 'bg-gray-100' }, '核心资产': { color: 'text-blue-800', bg: 'bg-blue-100' }, '压力仍存': { color: 'text-orange-800', bg: 'bg-orange-100' }, '趋势上行': { color: 'text-emerald-800', bg: 'bg-emerald-100' }, '高位': { color: 'text-red-800', bg: 'bg-red-100' }, }; let formatted = text; Object.entries(keywords).forEach(([keyword, styles]) => { // 创建正则,允许关键词中间有可选空格 const keywordWithOptionalSpaces = keyword.split('').join('\\s*'); const regex = new RegExp(keywordWithOptionalSpaces, 'g'); formatted = formatted.replace( regex, (match) => `${match.replace(/\s+/g, '')}` ); }); return formatted; }; export const DashboardPage: React.FC = () => { const navigate = useNavigate(); const [modules, setModules] = useState(INITIAL_DATA); const [isTemplateLibraryOpen, setIsTemplateLibraryOpen] = useState(false); const [isSaveTemplateOpen, setIsSaveTemplateOpen] = useState(false); const [templateName, setTemplateName] = useState(''); const [rightPanelVisible, setRightPanelVisible] = useState(true); // 默认展开 const [shouldRenderCharts, setShouldRenderCharts] = useState(true); // 默认渲染图表 const [isEditMode, setIsEditMode] = useState(false); // 编辑模式状态 const [showSortSuccessAlert, setShowSortSuccessAlert] = useState(false); // 排序成功提示 const [dimensionRatings, setDimensionRatings] = useState>({ valuation: 0, capital: 0, macro: 0, assets: 0, }); // 维度评分 const [expandedGroups, setExpandedGroups] = useState>({ valuation: true, // 默认展开 capital: true, macro: true, assets: true, }); // 拖拽排序相关状态 const [draggedGroupId, setDraggedGroupId] = useState(null); const [draggedItemId, setDraggedItemId] = useState(null); const dragItemRef = useRef(null); const dragOverItemRef = useRef(null); const dragGroupRef = useRef(null); const toggleGroup = (id: string) => { setExpandedGroups(prev => ({ ...prev, [id]: !prev[id] })); }; // 拖拽开始 const handleItemDragStart = (e: React.DragEvent, groupId: string, itemIndex: number) => { console.log('拖拽开始', { isEditMode, groupId, itemIndex }); if (!isEditMode) { e.preventDefault(); return; } dragItemRef.current = itemIndex; dragGroupRef.current = groupId; setDraggedItemId(`${groupId}-${itemIndex}`); e.dataTransfer.effectAllowed = "move"; }; // 拖拽进入 const handleItemDragEnter = (e: React.DragEvent, groupId: string, itemIndex: number) => { if (!isEditMode) return; console.log('拖拽进入', { groupId, itemIndex, dragGroupRef: dragGroupRef.current }); e.preventDefault(); if (dragGroupRef.current === groupId) { dragOverItemRef.current = itemIndex; } }; // 拖拽结束 const handleItemDragEnd = (e: React.DragEvent, groupId: string) => { console.log('拖拽结束', { isEditMode, groupId, dragItemRef: dragItemRef.current, dragOverItemRef: dragOverItemRef.current, dragGroupRef: dragGroupRef.current }); e.preventDefault(); // 先保存 ref 值,因为会在重置后才执行 setModules 回调 const dragFromIndex = dragItemRef.current; const dragToIndex = dragOverItemRef.current; const dragFromGroup = dragGroupRef.current; if ( isEditMode && dragFromIndex !== null && dragToIndex !== null && dragFromGroup === groupId && dragFromIndex !== dragToIndex ) { console.log('开始执行拖拽交换', { dragFromIndex, dragToIndex }); setModules(prevModules => { console.log('当前modules:', prevModules); const newModules = [...prevModules]; const groupIndex = newModules.findIndex(m => m.id === groupId); console.log('找到groupIndex:', groupIndex); if (groupIndex !== -1 && newModules[groupIndex].items) { const items = [...newModules[groupIndex].items]; console.log('items长度:', items.length); console.log('dragFromIndex:', dragFromIndex); console.log('dragToIndex:', dragToIndex); // 安全检查:确保索引有效 if (dragFromIndex >= 0 && dragFromIndex < items.length) { const draggedItem = items[dragFromIndex]; console.log('拖拽的item:', draggedItem); // 确保 draggedItem 存在 if (draggedItem) { items.splice(dragFromIndex, 1); // 计算正确的插入位置 const insertIndex = Math.min(dragToIndex, items.length); console.log('插入位置:', insertIndex); items.splice(insertIndex, 0, draggedItem); newModules[groupIndex] = { ...newModules[groupIndex], items }; console.log('更新后的items:', items); } } } console.log('返回新modules:', newModules); return newModules; }); // 显示成功提示 setShowSortSuccessAlert(true); setTimeout(() => { setShowSortSuccessAlert(false); }, 2000); } // 始终重置拖拽状态 dragItemRef.current = null; dragOverItemRef.current = null; dragGroupRef.current = null; setDraggedItemId(null); }; // 拖拽悬停 const handleItemDragOver = (e: React.DragEvent) => { if (!isEditMode) return; e.preventDefault(); }; const handleScrollTo = (elementId: string, itemId: string, e: React.MouseEvent) => { e.stopPropagation(); // 先显示右侧面板 setRightPanelVisible(true); // 左侧滚动到对应的指标项 requestAnimationFrame(() => { const leftElement = document.getElementById(`left-${itemId}`); if (leftElement) { leftElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); // 延迟300ms后渲染图表,等待动画完成 setTimeout(() => { setShouldRenderCharts(true); // 再延迟一帧滚动到对应位置 requestAnimationFrame(() => { const element = document.getElementById(elementId); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }, 300); }; const handleGroupClick = (groupId: string, e: React.MouseEvent) => { e.stopPropagation(); // 先显示右侧面板 setRightPanelVisible(true); // 左侧滚动到对应的维度卡片 requestAnimationFrame(() => { const leftElement = document.getElementById(`left-group-${groupId}`); if (leftElement) { leftElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); // 延迟300ms后渲染图表 setTimeout(() => { setShouldRenderCharts(true); const group = modules.find(m => m.id === groupId); if (group && group.items.length > 0) { requestAnimationFrame(() => { const element = document.getElementById(`chart-${group.items[0].id}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); } }, 300); }; const handleApplyTemplate = () => { setIsTemplateLibraryOpen(false); }; // 保存模板 const handleSaveTemplate = () => { if (templateName.trim()) { // 这里可以添加实际的保存逻辑 console.log('保存模板:', templateName); setIsSaveTemplateOpen(false); setTemplateName(''); // 可以显示成功提示 } }; // 关闭右侧面板时,重置图表渲染状态 const handleCloseRightPanel = () => { setRightPanelVisible(false); setShouldRenderCharts(false); }; const getDimensionColor = (id: string) => { const colorMap: Record = { 'valuation': { left: 'bg-blue-500', bg: 'bg-white', hover: 'hover:bg-gray-50', icon: 'bg-blue-50 text-blue-500' }, 'capital': { left: 'bg-purple-500', bg: 'bg-white', hover: 'hover:bg-gray-50', icon: 'bg-purple-50 text-purple-500' }, 'macro': { left: 'bg-teal-500', bg: 'bg-white', hover: 'hover:bg-gray-50', icon: 'bg-teal-50 text-teal-500' }, 'assets': { left: 'bg-indigo-500', bg: 'bg-white', hover: 'hover:bg-gray-50', icon: 'bg-indigo-50 text-indigo-500' }, }; return colorMap[id] || colorMap['valuation']; }; // 删除整个维度 const handleDeleteGroup = (groupId: string, e: React.MouseEvent) => { e.stopPropagation(); if (window.confirm('确定要删除整个维度吗?')) { setModules(prevModules => prevModules.filter(m => m.id !== groupId)); } }; // 删除单个指标 const handleDeleteItem = (groupId: string, itemId: string, e: React.MouseEvent) => { e.stopPropagation(); if (window.confirm('确定要删除这个指标吗?')) { setModules(prevModules => { return prevModules.map(group => { if (group.id === groupId) { return { ...group, items: group.items.filter(item => item && item.id !== itemId) }; } return group; }); }); } }; // 设置维度评分 const handleRatingChange = (groupId: string, rating: number, e: React.MouseEvent) => { e.stopPropagation(); setDimensionRatings(prev => ({ ...prev, [groupId]: rating })); }; return (
{/* 排序成功提示 */} {showSortSuccessAlert && (
排序已保存
)}
{/* Top Header */}
navigate('/')} className="cursor-pointer text-slate-500 hover:text-blue-600 transition-colors font-medium whitespace-nowrap">首页
贵州茅台 600519.SH
{/* 编辑模式提示 - 居中显示 */} {isEditMode && (
编辑模式已开启,可排序、删除等操作
)}
{/* Main Content - Split Layout */}
{/* Left Panel: Rating Snapshot Table */}

资产评级表

{rightPanelVisible && ( )}
{!rightPanelVisible && (
综合估值、资金、宏观、基本面四大维度,多视角量化评估资产投资价值
)}
{/* 维度布局:右侧展开时在大屏幕(>1440px)显示2列,否则1列;未展开时两列 */}
{modules.map((group) => { const colors = getDimensionColor(group.id); return (
{/* 维度标题 */}
!isEditMode && handleGroupClick(group.id, e)} className={`relative px-4 py-3 border-b border-slate-200 ${!isEditMode ? 'cursor-pointer' : ''} ${colors.hover} transition-colors`} >
{/* 第一行:图标、标题、评分、删除按钮 */}
{group.icon}

{group.dimension}

{/* 编辑模式下显示评分星星 */} {isEditMode && (
{[1, 2, 3, 4, 5].map((star) => ( ))}
)} {/* 编辑模式下显示删除按钮 */} {isEditMode && ( )}
{/* 第二行:维度描述 */} {group.description && (

{group.description}

)}
{/* 指标列表 */}
{group.items?.map((item, itemIndex) => { // 跳过 undefined 的 item if (!item) return null; return (
handleItemDragStart(e, group.id, itemIndex)} onDragEnter={(e) => handleItemDragEnter(e, group.id, itemIndex)} onDragEnd={(e) => handleItemDragEnd(e, group.id)} onDragOver={handleItemDragOver} onClick={(e) => { if (!isEditMode) { handleScrollTo(`chart-${item.id}`, item.id, e); } }} className={`p-4 hover:bg-slate-50 transition-all ${ isEditMode ? 'cursor-move' : 'cursor-pointer' } ${draggedItemId === `${group.id}-${itemIndex}` ? 'opacity-50' : ''} group`} >
{/* 指标标题 */}

{item.label}

{/* 摘要 - 编辑模式下隐藏 */} {!isEditMode && (

{item.summary}

)}
{/* 编辑模式下显示删除按钮 */} {isEditMode && ( )}
); })} {/* 编辑模式下显示"添加视角"按钮 */} {isEditMode && (
)}
); })} {/* 编辑模式下显示"添加维度"按钮 */} {isEditMode && (
)}
{/* Right Panel: Charts */} {rightPanelVisible && (
{modules.map((group) => (
{/* Section Header */}

{group.dimension}

{/* Charts List */}
{group.items.map((item) => (

{item.label}

{group.dimension}
{/* Chart */}
{shouldRenderCharts ? ( ) : (
加载中...
)}
{/* Summary */}

{item.summary}

))}
))}
)}
{/* Template Library Modal */} {isTemplateLibraryOpen && ( setIsTemplateLibraryOpen(false)} onApply={handleApplyTemplate} /> )} {/* Save Template Modal */} {isSaveTemplateOpen && (
{/* Header */}

保存为模板

输入模板名称以保存当前配置

{/* Content */}
setTemplateName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && templateName.trim()) { handleSaveTemplate(); } }} placeholder="例如:茅台估值分析模板" className="w-full px-4 py-2.5 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" autoFocus />
{/* Footer */}
)}
); }; // Chart Component const DetailChart: React.FC<{data: any[], color: string, label: string}> = ({ data, color, label }) => ( ); // Template Library Modal const TemplateLibraryModal: React.FC<{onClose: () => void, onApply: () => void}> = ({ onClose, onApply }) => { const [activeTab, setActiveTab] = useState<'general' | 'my'>('general'); const GENERAL_TEMPLATES = [ { id: 't1', title: '个股通用深度模板', desc: '包含估值、资金、宏观、财务四大核心维度', icon: , tags: ['全市场', '通用'] }, { id: 't2', title: '快速诊断模板', desc: '精简版模板,聚焦于PE/PB band与短期资金流向', icon: , tags: ['短线', '效率'] } ]; const MY_TEMPLATES = [ { id: 'm1', title: '宁德时代评级表', desc: '包含新能源行业特色指标、产能利用率、技术路线分析', icon: , tags: ['新能源', '自定义'], date: '2024-03-15' }, { id: 'm2', title: '茅台估值分析模板', desc: '聚焦消费品估值体系、品牌溢价、渠道分析', icon: , tags: ['消费', '自定义'], date: '2024-03-10' } ]; const [selectedTemplate, setSelectedTemplate] = useState(null); const currentTemplates = activeTab === 'general' ? GENERAL_TEMPLATES : MY_TEMPLATES; return (

模板库

{/* Tabs */}
{currentTemplates.map((template) => (
setSelectedTemplate(template.id)} className={`p-4 rounded-xl border cursor-pointer transition-all ${ selectedTemplate === template.id ? 'bg-blue-50 border-blue-500 ring-1 ring-blue-500' : 'bg-white border-slate-200 hover:border-blue-300' }`} >
{template.icon}

{template.title}

{activeTab === 'my' && 'date' in template && ( {(template as any).date} )}

{template.desc}

{template.tags.map(tag => ( {tag} ))}
))}
); };