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(false); // 默认隐藏 const [shouldRenderCharts, setShouldRenderCharts] = useState(false); // 控制图表渲染 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 && ( 编辑模式已开启,可排序、删除等操作 setIsEditMode(false)} className="text-xs px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium whitespace-nowrap" > 完成 )} setIsTemplateLibraryOpen(true)} className="flex items-center gap-1 sm:gap-1.5 text-slate-600 hover:text-blue-600 transition-colors text-sm font-medium group" > 模板库 setIsSaveTemplateOpen(true)} className="flex items-center gap-1 sm:gap-1.5 text-slate-600 hover:text-blue-600 transition-colors text-sm font-medium group" > 另存为 {/* Main Content - Split Layout */} {/* Left Panel: Rating Snapshot Table */} 资产评级表 { e.stopPropagation(); const newMode = !isEditMode; console.log('切换编辑模式:', newMode); setIsEditMode(newMode); }} className={`p-1.5 rounded-lg transition-colors group ${ isEditMode ? 'text-blue-600 bg-blue-50' : 'text-slate-600 hover:text-blue-600 hover:bg-blue-50' }`} title="设置维度和视角" > {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) => ( handleRatingChange(group.id, star, e)} className="transition-colors" title={`${star}星`} > ))} )} {/* 编辑模式下显示删除按钮 */} {isEditMode && ( handleDeleteGroup(group.id, e)} className="p-1.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" title="删除维度" > )} {/* 第二行:维度描述 */} {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 && ( handleDeleteItem(group.id, item.id, e)} className="p-1.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors flex-shrink-0" title="删除指标" > )} ); })} {/* 编辑模式下显示"添加视角"按钮 */} {isEditMode && ( { e.stopPropagation(); // TODO: 打开添加视角的对话框 alert('添加视角功能'); }} className="w-full flex items-center justify-center gap-2 py-2.5 text-sm text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors border border-dashed border-blue-300 hover:border-blue-400" > 添加视角 )} ); })} {/* 编辑模式下显示"添加维度"按钮 */} {isEditMode && ( { e.stopPropagation(); // TODO: 打开添加维度的对话框 alert('添加维度功能'); }} className="w-full h-full min-h-[200px] flex flex-col items-center justify-center gap-3 text-slate-500 hover:text-blue-600 transition-colors p-8" > 添加维度 )} {/* 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} { e.stopPropagation(); navigate('/detail'); }} className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors flex-shrink-0" title="查看详情" > Ask ))} ))} )} {/* 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 */} { setIsSaveTemplateOpen(false); setTemplateName(''); }} className="px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 rounded-lg transition-colors" > 取消 保存 )} ); }; // 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 */} setActiveTab('general')} className={`py-3 text-sm font-medium border-b-2 transition-colors ${ activeTab === 'general' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700' }`} > 通用模板 setActiveTab('my')} className={`py-3 text-sm font-medium border-b-2 transition-colors ${ activeTab === 'my' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700' }`} > 我的模板 {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} ))} ))} 取消 应用模板 ); };
{group.description}
{item.summary}
输入模板名称以保存当前配置
{template.desc}