diff --git a/src/views/DataBrowser/KLineChartView.tsx b/src/views/DataBrowser/KLineChartView.tsx new file mode 100644 index 00000000..e08ac730 --- /dev/null +++ b/src/views/DataBrowser/KLineChartView.tsx @@ -0,0 +1,396 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + Box, + VStack, + HStack, + Text, + Button, + ButtonGroup, + Flex, + Icon, + Tooltip, +} from '@chakra-ui/react'; +import { init, dispose } from 'klinecharts'; +import { + FaExpand, + FaCompress, + FaCamera, + FaRedo, +} from 'react-icons/fa'; +import { MetricDataPoint } from '@services/categoryService'; + +// 黑金主题配色 +const themeColors = { + bg: { + primary: '#0a0a0a', + secondary: '#1a1a1a', + card: '#1e1e1e', + }, + text: { + primary: '#ffffff', + secondary: '#b8b8b8', + muted: '#808080', + gold: '#D4AF37', + }, + border: { + default: 'rgba(255, 255, 255, 0.1)', + gold: 'rgba(212, 175, 55, 0.3)', + }, + primary: { + gold: '#D4AF37', + goldLight: '#F4E3A7', + }, +}; + +interface KLineChartViewProps { + data: MetricDataPoint[]; + metricName: string; + unit: string; + frequency: string; +} + +type TimeRange = '1M' | '3M' | '6M' | '1Y' | 'YTD' | 'ALL'; + +const KLineChartView: React.FC = ({ + data, + metricName, + unit, + frequency, +}) => { + const chartContainerRef = useRef(null); + const chartRef = useRef(null); + const [isFullscreen, setIsFullscreen] = useState(false); + const [selectedRange, setSelectedRange] = useState('ALL'); + + // 初始化图表 + useEffect(() => { + if (!chartContainerRef.current || data.length === 0) return; + + // 创建图表实例 + const chart = init(chartContainerRef.current, { + styles: { + grid: { + horizontal: { + color: 'rgba(255, 255, 255, 0.05)', + }, + vertical: { + color: 'rgba(255, 255, 255, 0.05)', + }, + }, + candle: { + type: 'line', // 使用折线图模式 + line: { + upColor: themeColors.primary.gold, + downColor: themeColors.primary.gold, + style: 'solid', + size: 2, + }, + }, + crosshair: { + horizontal: { + line: { + color: themeColors.primary.gold, + style: 'dashed', + size: 1, + }, + text: { + backgroundColor: themeColors.primary.gold, + color: themeColors.bg.primary, + }, + }, + vertical: { + line: { + color: themeColors.primary.gold, + style: 'dashed', + size: 1, + }, + text: { + backgroundColor: themeColors.primary.gold, + color: themeColors.bg.primary, + }, + }, + }, + xAxis: { + axisLine: { + color: themeColors.border.default, + }, + tickLine: { + color: themeColors.border.default, + }, + tickText: { + color: themeColors.text.secondary, + }, + }, + yAxis: { + axisLine: { + color: themeColors.border.default, + }, + tickLine: { + color: themeColors.border.default, + }, + tickText: { + color: themeColors.text.secondary, + }, + }, + }, + }); + + // 转换数据格式 + const chartData = data + .filter((item) => item.value !== null) + .map((item) => ({ + timestamp: new Date(item.date).getTime(), + open: item.value, + high: item.value, + low: item.value, + close: item.value, + })) + .sort((a, b) => a.timestamp - b.timestamp); + + // 设置数据 + chart?.applyNewData(chartData); + + chartRef.current = chart; + + // 清理 + return () => { + if (chartRef.current) { + dispose(chartContainerRef.current!); + chartRef.current = null; + } + }; + }, [data]); + + // 时间范围筛选 + const handleTimeRangeChange = (range: TimeRange) => { + setSelectedRange(range); + // TODO: 实现时间范围筛选逻辑 + }; + + // 重置缩放 + const handleReset = () => { + chartRef.current?.zoomAtCoordinate({ x: 0.5 }, 1); + setSelectedRange('ALL'); + }; + + // 截图功能 + const handleScreenshot = () => { + if (!chartRef.current) return; + const imageUrl = chartRef.current.getConvertPictureUrl(); + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `${metricName}_${new Date().toISOString().split('T')[0]}.png`; + link.click(); + }; + + // 全屏切换 + const toggleFullscreen = () => { + if (!chartContainerRef.current) return; + + if (!isFullscreen) { + if (chartContainerRef.current.requestFullscreen) { + chartContainerRef.current.requestFullscreen(); + } + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } + } + setIsFullscreen(!isFullscreen); + }; + + // 计算统计数据 + const stats = React.useMemo(() => { + const values = data.filter((item) => item.value !== null).map((item) => item.value as number); + + if (values.length === 0) { + return { min: 0, max: 0, avg: 0, latest: 0, change: 0, changePercent: 0 }; + } + + const min = Math.min(...values); + const max = Math.max(...values); + const avg = values.reduce((sum, val) => sum + val, 0) / values.length; + const latest = values[values.length - 1]; + const first = values[0]; + const change = latest - first; + const changePercent = first !== 0 ? (change / first) * 100 : 0; + + return { min, max, avg, latest, change, changePercent }; + }, [data]); + + // 格式化数字 + const formatNumber = (num: number) => { + if (Math.abs(num) >= 1e9) { + return (num / 1e9).toFixed(2) + 'B'; + } + if (Math.abs(num) >= 1e6) { + return (num / 1e6).toFixed(2) + 'M'; + } + if (Math.abs(num) >= 1e3) { + return (num / 1e3).toFixed(2) + 'K'; + } + return num.toFixed(2); + }; + + return ( + + {/* 工具栏 */} + + {/* 时间范围选择 */} + + {(['1M', '3M', '6M', '1Y', 'YTD', 'ALL'] as TimeRange[]).map((range) => ( + + ))} + + + {/* 图表操作 */} + + + + + + + + + + + + + + {/* 统计数据 */} + + + + 最新值 + + + {formatNumber(stats.latest)} {unit} + + = 0 ? '#00ff88' : '#ff4444'} + fontSize="xs" + fontWeight="bold" + > + {stats.change >= 0 ? '+' : ''} + {formatNumber(stats.change)} ({stats.changePercent.toFixed(2)}%) + + + + + + 平均值 + + + {formatNumber(stats.avg)} {unit} + + + + + + 最高值 + + + {formatNumber(stats.max)} {unit} + + + + + + 最低值 + + + {formatNumber(stats.min)} {unit} + + + + + + 数据点数 + + + {data.filter((item) => item.value !== null).length} + + + + + + 频率 + + + {frequency} + + + + + {/* 图表容器 */} + + + {/* 提示信息 */} + + + 💡 提示:滚轮缩放,拖拽移动视图 + + 数据来源: {metricName} + + + ); +}; + +export default KLineChartView; diff --git a/src/views/DataBrowser/MetricDataModal.tsx b/src/views/DataBrowser/MetricDataModal.tsx index 06ec6cdd..74ef2bc2 100644 --- a/src/views/DataBrowser/MetricDataModal.tsx +++ b/src/views/DataBrowser/MetricDataModal.tsx @@ -31,7 +31,7 @@ import { } from '@chakra-ui/react'; import { FaTable, FaChartLine, FaCalendar, FaDownload } from 'react-icons/fa'; import { fetchMetricData, MetricDataResponse, TreeMetric } from '@services/categoryService'; -import TradingViewChart from './TradingViewChart'; +import KLineChartView from './KLineChartView'; // 黑金主题配色 const themeColors = { @@ -277,10 +277,10 @@ const MetricDataModal: React.FC = ({ isOpen, onClose, metr - {/* 折线图 - 使用 TradingView Lightweight Charts */} + {/* 折线图 - 使用 KLineChart */} {metricData && metricData.data.length > 0 ? ( -