834 lines
43 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, 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: <BarChart3 size={18} />,
tags: ['A股市场', '估值'],
description: '从估值水平、历史分位、质量溢价等多角度评估当前估值合理性',
},
{
...STOCK_DATA_CONFIG.macro,
icon: <Globe size={18} />,
tags: ['A股市场', '宏观'],
description: '结合宏观经济环境、无风险利率、风险溢价等因素分析投资胜率',
},
{
...STOCK_DATA_CONFIG.assets,
icon: <TrendingUp size={18} />,
tags: ['A股市场', '基本面'],
description: '跟踪产业链上下游走势、竞争格局变化等基本面相关资产表现',
},
{
...STOCK_DATA_CONFIG.capital,
icon: <PieChart size={18} />,
tags: ['A股市场', '资金面'],
description: '观察市场资金流向、成交活跃度等资金面动态变化',
}
];
// 格式化摘要文本,添加标签高亮
const formatSummary = (text: string): string => {
const keywords: Record<string, { color: string, bg: string }> = {
'质量领跑': { 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) => `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${styles.bg} ${styles.color} mx-1">${match.replace(/\s+/g, '')}</span>`
);
});
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<Record<string, number>>({
valuation: 0,
capital: 0,
macro: 0,
assets: 0,
}); // 维度评分
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({
valuation: true, // 默认展开
capital: true,
macro: true,
assets: true,
});
// 拖拽排序相关状态
const [draggedGroupId, setDraggedGroupId] = useState<string | null>(null);
const [draggedItemId, setDraggedItemId] = useState<string | null>(null);
const dragItemRef = useRef<number | null>(null);
const dragOverItemRef = useRef<number | null>(null);
const dragGroupRef = useRef<string | null>(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 getDimensionColor = (id: string) => {
const colorMap: Record<string, { left: string, bg: string, hover: string, icon: string }> = {
'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 (
<div className="flex min-h-screen bg-slate-50 overflow-hidden relative">
{/* 排序成功提示 */}
{showSortSuccessAlert && (
<div className="fixed top-20 left-1/2 -translate-x-1/2 z-50 animate-fade-in">
<div className="bg-green-50 border border-green-200 rounded-lg px-6 py-3 shadow-lg flex items-center gap-2">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm text-green-700 font-medium">
</span>
</div>
</div>
)}
<div className="flex-1 flex flex-col min-w-0 h-screen">
{/* Top Header */}
<header className="h-16 bg-white border-b border-slate-200 px-4 sm:px-6 flex items-center justify-between shrink-0 z-20">
<div className="flex items-center gap-2 sm:gap-3 text-sm min-w-0">
<span onClick={() => navigate('/')} className="cursor-pointer text-slate-500 hover:text-blue-600 transition-colors font-medium whitespace-nowrap"></span>
<ChevronRight size={14} className="text-slate-300 flex-shrink-0" />
<div className="flex items-center gap-2 min-w-0">
<span className="font-bold text-base sm:text-lg text-slate-900 tracking-tight truncate"></span>
<span className="bg-gray-100 text-slate-600 px-1.5 sm:px-2 py-0.5 rounded text-xs font-mono font-semibold border border-gray-200 whitespace-nowrap">600519.SH</span>
</div>
</div>
{/* 编辑模式提示 - 居中显示 */}
{isEditMode && (
<div className="flex-1 flex justify-center mx-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-2">
<GripVertical size={16} className="text-blue-600" />
<span className="text-sm text-blue-700 font-medium whitespace-nowrap">
</span>
</div>
<button
onClick={() => 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"
>
</button>
</div>
</div>
)}
<div className="flex items-center gap-2 sm:gap-5">
<button
onClick={() => 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"
>
<LayoutTemplate size={18} className="group-hover:scale-110 transition-transform flex-shrink-0" />
<span className="hidden sm:inline"></span>
</button>
<button
onClick={() => 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"
>
<Save size={18} className="group-hover:scale-110 transition-transform flex-shrink-0" />
<span className="hidden sm:inline"></span>
</button>
</div>
</header>
{/* Main Content - Split Layout */}
<div className="flex-1 flex overflow-hidden">
{/* Left Panel: Rating Snapshot Table */}
<div className="bg-white border-r border-slate-200 flex flex-col z-10 shadow-sm transition-all duration-300 w-full lg:w-[380px] xl:w-[480px] 2xl:w-[720px]">
<div className="flex flex-col flex-1 min-h-0">
<div className="p-4 border-b border-slate-100 bg-white shrink-0">
<div className="flex items-center justify-center">
<h2 className="text-base sm:text-lg font-bold text-slate-900 flex items-center gap-2">
<span style={{ fontSize: '26px' }}></span>
<button
onClick={(e) => {
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="设置维度和视角"
>
<Settings size={16} className="group-hover:rotate-90 transition-transform duration-300" />
</button>
</h2>
</div>
<div className="text-sm text-slate-400 mt-3 text-center"></div>
</div>
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
{/* 维度布局:右侧展开时在大屏幕(>1440px)显示2列否则1列 */}
<div className="grid gap-4 grid-cols-1 2xl:grid-cols-2">
{modules.map((group) => {
const colors = getDimensionColor(group.id);
return (
<div
key={group.id}
className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden hover:shadow-md transition-all group/card"
>
{/* 维度标题 */}
<div
onClick={(e) => !isEditMode && handleGroupClick(group.id, e)}
className={`relative px-4 py-3 border-b border-slate-200 ${!isEditMode ? 'cursor-pointer' : ''} ${colors.hover} transition-colors`}
>
<div className={`absolute left-0 top-0 bottom-0 w-1 ${colors.left}`}></div>
<div className="flex flex-col gap-2 pl-2">
{/* 第一行:图标、标题、评分、删除按钮 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`p-1.5 ${colors.icon} rounded-lg`}>
{group.icon}
</div>
<h2 className="text-sm font-bold text-slate-900">
{group.dimension}
</h2>
</div>
<div className="flex items-center gap-2">
{/* 编辑模式下显示评分星星 */}
{isEditMode && (
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
onClick={(e) => handleRatingChange(group.id, star, e)}
className="transition-colors"
title={`${star}`}
>
<Star
size={16}
className={`${
star <= dimensionRatings[group.id]
? 'text-yellow-500 fill-yellow-500'
: 'text-gray-300'
}`}
/>
</button>
))}
</div>
)}
{/* 编辑模式下显示删除按钮 */}
{isEditMode && (
<button
onClick={(e) => handleDeleteGroup(group.id, e)}
className="p-1.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="删除维度"
>
<X size={16} />
</button>
)}
</div>
</div>
{/* 第二行:维度描述 */}
{group.description && (
<p className="text-xs text-slate-400 pl-9 leading-relaxed">
{group.description}
</p>
)}
</div>
</div>
{/* 指标列表 */}
<div className="divide-y divide-slate-100" id={`left-group-${group.id}`}>
{group.items?.map((item, itemIndex) => {
// 跳过 undefined 的 item
if (!item) return null;
return (
<div
key={item.id}
id={`left-${item.id}`}
draggable={isEditMode}
onDragStart={(e) => 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`}
>
<div className="flex items-start gap-2">
<GripVertical
size={14}
className={`text-slate-300 mt-0.5 flex-shrink-0 transition-opacity ${
isEditMode ? 'opacity-100' : 'opacity-0'
}`}
/>
<div className="flex-1 min-w-0">
{/* 指标标题 */}
<h3 className="text-sm font-bold text-slate-900 mb-2 group-hover:text-blue-600 transition-colors">
{item.label}
</h3>
{/* 摘要 - 编辑模式下隐藏 */}
{!isEditMode && (
<p className="text-xs text-slate-600 leading-relaxed">
{item.summary}
</p>
)}
</div>
{/* 编辑模式下显示删除按钮 */}
{isEditMode && (
<button
onClick={(e) => 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="删除指标"
>
<X size={16} />
</button>
)}
</div>
</div>
);
})}
{/* 编辑模式下显示"添加视角"按钮 */}
{isEditMode && (
<div className="p-4 border-t border-slate-100 opacity-0 group-hover/card:opacity-100 transition-opacity">
<button
onClick={(e) => {
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"
>
<Plus size={16} />
<span className="font-medium"></span>
</button>
</div>
)}
</div>
</div>
);
})}
{/* 编辑模式下显示"添加维度"按钮 */}
{isEditMode && (
<div className="bg-white rounded-xl shadow-sm border-2 border-dashed border-slate-300 overflow-hidden hover:shadow-md hover:border-blue-400 transition-all">
<button
onClick={(e) => {
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"
>
<div className="w-12 h-12 rounded-full bg-slate-100 hover:bg-blue-50 flex items-center justify-center transition-colors">
<Plus size={24} />
</div>
<span className="text-sm font-medium"></span>
</button>
</div>
)}
</div>
</div>
</div>
</div>
{/* Right Panel: Charts */}
<div className="hidden lg:flex flex-1 relative flex-col min-w-0 bg-slate-50">
<div className="flex-1 overflow-y-auto p-6 scroll-smooth">
<div className="max-w-4xl mx-auto space-y-12">
{modules.map((group) => (
<div key={group.id} id={`group-${group.id}`} className="space-y-6 scroll-mt-6">
{/* Section Header */}
<div className="flex items-center gap-3 pl-2 border-l-4 border-slate-900">
<h2 className="text-xl font-bold text-slate-900">{group.dimension}</h2>
</div>
{/* Charts List */}
<div className="space-y-6">
{group.items.map((item) => (
<div
id={`chart-${item.id}`}
key={item.id}
className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 hover:shadow-md transition-all scroll-mt-24"
>
<div className="mb-4">
<h3 className="text-base font-bold text-slate-900">{item.label}</h3>
<span className="text-xs text-slate-400">{group.dimension}</span>
</div>
{/* Chart */}
<div className="h-80 w-full mb-4">
{shouldRenderCharts ? (
<DetailChart data={item.chartData} color={item.color} label={item.label} />
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-50 rounded-lg">
<div className="text-slate-400 text-sm">...</div>
</div>
)}
</div>
{/* Summary */}
<div className="bg-slate-50 rounded-xl p-4 border border-slate-100 flex items-start justify-between gap-3">
<p className="text-sm text-slate-700 font-medium italic flex-1 text-center">
{item.summary}
</p>
<button
onClick={(e) => {
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="查看详情"
>
<span>Ask</span>
<ArrowUpRight size={14} />
</button>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Template Library Modal */}
{isTemplateLibraryOpen && (
<TemplateLibraryModal onClose={() => setIsTemplateLibraryOpen(false)} onApply={handleApplyTemplate} />
)}
{/* Save Template Modal */}
{isSaveTemplateOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md">
{/* Header */}
<div className="p-6 border-b border-slate-200">
<h2 className="text-xl font-bold text-slate-900"></h2>
<p className="text-sm text-slate-500 mt-1"></p>
</div>
{/* Content */}
<div className="p-6">
<label className="block text-sm font-medium text-slate-700 mb-2">
</label>
<input
type="text"
value={templateName}
onChange={(e) => 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
/>
</div>
{/* Footer */}
<div className="p-6 border-t border-slate-200 flex items-center justify-end gap-3">
<button
onClick={() => {
setIsSaveTemplateOpen(false);
setTemplateName('');
}}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
</button>
<button
onClick={handleSaveTemplate}
disabled={!templateName.trim()}
className={`px-6 py-2 text-sm font-bold text-white rounded-lg transition-colors ${
templateName.trim()
? 'bg-blue-600 hover:bg-blue-700'
: 'bg-slate-300 cursor-not-allowed'
}`}
>
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
// Chart Component
const DetailChart: React.FC<{data: any[], color: string, label: string}> = ({ data, color, label }) => (
<StockChart data={data} color={color} legend={`贵州茅台_${label.split(' ')[0]}`} />
);
// 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: <FileText size={20} className="text-blue-600" />, tags: ['全市场', '通用'] },
{ id: 't2', title: '快速诊断模板', desc: '精简版模板,聚焦于PE/PB band与短期资金流向', icon: <TrendingUp size={20} className="text-amber-500" />, tags: ['短线', '效率'] }
];
const MY_TEMPLATES = [
{ id: 'm1', title: '宁德时代评级表', desc: '包含新能源行业特色指标、产能利用率、技术路线分析', icon: <FileText size={20} className="text-emerald-600" />, tags: ['新能源', '自定义'], date: '2024-03-15' },
{ id: 'm2', title: '茅台估值分析模板', desc: '聚焦消费品估值体系、品牌溢价、渠道分析', icon: <FileText size={20} className="text-purple-600" />, tags: ['消费', '自定义'], date: '2024-03-10' }
];
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const currentTemplates = activeTab === 'general' ? GENERAL_TEMPLATES : MY_TEMPLATES;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm">
<div className="bg-white w-full max-w-2xl max-h-[90vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden">
<div className="h-14 sm:h-16 border-b border-slate-100 flex items-center justify-between px-4 sm:px-6">
<h2 className="text-base sm:text-lg font-bold text-slate-900"></h2>
<button onClick={onClose} className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full"></button>
</div>
{/* Tabs */}
<div className="border-b border-slate-200 px-4 sm:px-6">
<div className="flex gap-6">
<button
onClick={() => 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'
}`}
>
</button>
<button
onClick={() => 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'
}`}
>
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
<div className="space-y-3">
{currentTemplates.map((template) => (
<div
key={template.id}
onClick={() => 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'
}`}
>
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-slate-50">{template.icon}</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="text-sm font-bold">{template.title}</h4>
{activeTab === 'my' && 'date' in template && (
<span className="text-xs text-slate-400">{(template as any).date}</span>
)}
</div>
<p className="text-xs text-slate-500 mb-2">{template.desc}</p>
<div className="flex gap-2">
{template.tags.map(tag => (
<span key={tag} className="px-2 py-0.5 text-xs rounded bg-slate-100 text-slate-500">{tag}</span>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
<div className="h-16 border-t border-slate-200 px-4 sm:px-6 flex items-center justify-end gap-3">
<button onClick={onClose} className="px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 rounded-lg"></button>
<button
disabled={!selectedTemplate}
onClick={onApply}
className={`px-6 py-2 text-sm font-bold text-white rounded-lg ${
selectedTemplate ? 'bg-blue-600 hover:bg-blue-700' : 'bg-slate-300 cursor-not-allowed'
}`}
>
</button>
</div>
</div>
</div>
);
};