update pay function
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@@ -30,8 +30,8 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FaTable, FaChartLine, FaCalendar, FaDownload } from 'react-icons/fa';
|
import { FaTable, FaChartLine, FaCalendar, FaDownload } from 'react-icons/fa';
|
||||||
import ReactECharts from 'echarts-for-react';
|
|
||||||
import { fetchMetricData, MetricDataResponse, TreeMetric } from '@services/categoryService';
|
import { fetchMetricData, MetricDataResponse, TreeMetric } from '@services/categoryService';
|
||||||
|
import TradingViewChart from './TradingViewChart';
|
||||||
|
|
||||||
// 黑金主题配色
|
// 黑金主题配色
|
||||||
const themeColors = {
|
const themeColors = {
|
||||||
@@ -98,131 +98,7 @@ const MetricDataModal: React.FC<MetricDataModalProps> = ({ isOpen, onClose, metr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 准备图表数据
|
// 数据已经在 metricData 中,直接传递给 TradingViewChart
|
||||||
const chartOption = useMemo(() => {
|
|
||||||
if (!metricData || !metricData.data || metricData.data.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dates = metricData.data.map((item) => item.date);
|
|
||||||
const values = metricData.data.map((item) => item.value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
title: {
|
|
||||||
text: metricData.metric_name,
|
|
||||||
left: 'center',
|
|
||||||
textStyle: {
|
|
||||||
color: themeColors.text.gold,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
backgroundColor: themeColors.bg.card,
|
|
||||||
borderColor: themeColors.border.gold,
|
|
||||||
textStyle: {
|
|
||||||
color: themeColors.text.primary,
|
|
||||||
},
|
|
||||||
formatter: (params: any) => {
|
|
||||||
const param = params[0];
|
|
||||||
return `
|
|
||||||
<div style="padding: 8px;">
|
|
||||||
<div style="color: ${themeColors.text.gold}; font-weight: bold; margin-bottom: 4px;">
|
|
||||||
${param.name}
|
|
||||||
</div>
|
|
||||||
<div style="color: ${themeColors.text.secondary};">
|
|
||||||
${param.seriesName}: ${param.value !== null ? param.value.toLocaleString() : '-'} ${metricData.unit || ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '3%',
|
|
||||||
right: '4%',
|
|
||||||
bottom: '10%',
|
|
||||||
top: '15%',
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: dates,
|
|
||||||
axisLabel: {
|
|
||||||
color: themeColors.text.secondary,
|
|
||||||
rotate: 45,
|
|
||||||
fontSize: 10,
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: themeColors.border.default,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
name: metricData.unit || '',
|
|
||||||
nameTextStyle: {
|
|
||||||
color: themeColors.text.gold,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
color: themeColors.text.secondary,
|
|
||||||
formatter: (value: number) => value.toLocaleString(),
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: themeColors.border.default,
|
|
||||||
type: 'dashed',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: themeColors.border.default,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: metricData.metric_name,
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
symbol: 'circle',
|
|
||||||
symbolSize: 6,
|
|
||||||
lineStyle: {
|
|
||||||
color: themeColors.primary.gold,
|
|
||||||
width: 2,
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: themeColors.primary.gold,
|
|
||||||
borderColor: themeColors.primary.goldLight,
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: {
|
|
||||||
type: 'linear',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: 'rgba(212, 175, 55, 0.3)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: 'rgba(212, 175, 55, 0.05)',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: values,
|
|
||||||
connectNulls: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}, [metricData]);
|
|
||||||
|
|
||||||
// 导出CSV
|
// 导出CSV
|
||||||
const handleExportCSV = () => {
|
const handleExportCSV = () => {
|
||||||
@@ -401,24 +277,15 @@ const MetricDataModal: React.FC<MetricDataModalProps> = ({ isOpen, onClose, metr
|
|||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{/* 折线图 */}
|
{/* 折线图 - 使用 TradingView Lightweight Charts */}
|
||||||
<TabPanel p={4}>
|
<TabPanel p={4}>
|
||||||
{chartOption ? (
|
{metricData && metricData.data.length > 0 ? (
|
||||||
<Box>
|
<TradingViewChart
|
||||||
<ReactECharts
|
data={metricData.data}
|
||||||
option={chartOption}
|
metricName={metricData.metric_name}
|
||||||
style={{ height: '500px', width: '100%' }}
|
unit={metricData.unit}
|
||||||
opts={{ renderer: 'svg' }}
|
frequency={metricData.frequency}
|
||||||
/>
|
/>
|
||||||
<Text
|
|
||||||
textAlign="center"
|
|
||||||
color={themeColors.text.muted}
|
|
||||||
fontSize="sm"
|
|
||||||
mt={2}
|
|
||||||
>
|
|
||||||
共 {metricData.data.length} 条数据点
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
<Flex justify="center" align="center" py={20}>
|
<Flex justify="center" align="center" py={20}>
|
||||||
<Text color={themeColors.text.muted}>暂无数据</Text>
|
<Text color={themeColors.text.muted}>暂无数据</Text>
|
||||||
|
|||||||
471
src/views/DataBrowser/TradingViewChart.tsx
Normal file
471
src/views/DataBrowser/TradingViewChart.tsx
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Flex,
|
||||||
|
Icon,
|
||||||
|
useColorMode,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { createChart, IChartApi, ISeriesApi, LineData, Time } from 'lightweight-charts';
|
||||||
|
import {
|
||||||
|
FaExpand,
|
||||||
|
FaCompress,
|
||||||
|
FaCamera,
|
||||||
|
FaRedo,
|
||||||
|
FaCog,
|
||||||
|
} 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 TradingViewChartProps {
|
||||||
|
data: MetricDataPoint[];
|
||||||
|
metricName: string;
|
||||||
|
unit: string;
|
||||||
|
frequency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeRange = '1M' | '3M' | '6M' | '1Y' | 'YTD' | 'ALL';
|
||||||
|
|
||||||
|
const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
||||||
|
data,
|
||||||
|
metricName,
|
||||||
|
unit,
|
||||||
|
frequency,
|
||||||
|
}) => {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
|
const lineSeriesRef = useRef<ISeriesApi<'Line'> | null>(null);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [selectedRange, setSelectedRange] = useState<TimeRange>('ALL');
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current || data.length === 0) return;
|
||||||
|
|
||||||
|
// 创建图表
|
||||||
|
const chart = createChart(chartContainerRef.current, {
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
height: 500,
|
||||||
|
layout: {
|
||||||
|
background: { color: themeColors.bg.card },
|
||||||
|
textColor: themeColors.text.secondary,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
style: 1, // 实线
|
||||||
|
},
|
||||||
|
horzLines: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
style: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crosshair: {
|
||||||
|
mode: 1, // 正常十字线模式
|
||||||
|
vertLine: {
|
||||||
|
color: themeColors.primary.gold,
|
||||||
|
width: 1,
|
||||||
|
style: 3, // 虚线
|
||||||
|
labelBackgroundColor: themeColors.primary.gold,
|
||||||
|
},
|
||||||
|
horzLine: {
|
||||||
|
color: themeColors.primary.gold,
|
||||||
|
width: 1,
|
||||||
|
style: 3,
|
||||||
|
labelBackgroundColor: themeColors.primary.gold,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
borderColor: themeColors.border.default,
|
||||||
|
scaleMargins: {
|
||||||
|
top: 0.1,
|
||||||
|
bottom: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeScale: {
|
||||||
|
borderColor: themeColors.border.default,
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
rightOffset: 5,
|
||||||
|
barSpacing: 10,
|
||||||
|
minBarSpacing: 3,
|
||||||
|
fixLeftEdge: true,
|
||||||
|
fixRightEdge: true,
|
||||||
|
},
|
||||||
|
handleScroll: {
|
||||||
|
mouseWheel: true,
|
||||||
|
pressedMouseMove: true,
|
||||||
|
horzTouchDrag: true,
|
||||||
|
vertTouchDrag: true,
|
||||||
|
},
|
||||||
|
handleScale: {
|
||||||
|
axisPressedMouseMove: true,
|
||||||
|
mouseWheel: true,
|
||||||
|
pinch: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建折线系列
|
||||||
|
const lineSeries = chart.addLineSeries({
|
||||||
|
color: themeColors.primary.gold,
|
||||||
|
lineWidth: 2,
|
||||||
|
lineStyle: 0, // 实线
|
||||||
|
crosshairMarkerVisible: true,
|
||||||
|
crosshairMarkerRadius: 6,
|
||||||
|
crosshairMarkerBorderColor: themeColors.primary.goldLight,
|
||||||
|
crosshairMarkerBackgroundColor: themeColors.primary.gold,
|
||||||
|
lastValueVisible: true,
|
||||||
|
priceLineVisible: true,
|
||||||
|
priceLineColor: themeColors.primary.gold,
|
||||||
|
priceLineWidth: 1,
|
||||||
|
priceLineStyle: 3, // 虚线
|
||||||
|
title: metricName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const chartData: LineData[] = data
|
||||||
|
.filter((item) => item.value !== null)
|
||||||
|
.map((item) => ({
|
||||||
|
time: item.date as Time,
|
||||||
|
value: item.value as number,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
// 确保时间从左到右递增
|
||||||
|
const timeA = new Date(a.time as string).getTime();
|
||||||
|
const timeB = new Date(b.time as string).getTime();
|
||||||
|
return timeA - timeB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置数据
|
||||||
|
lineSeries.setData(chartData);
|
||||||
|
|
||||||
|
// 自动缩放到合适的视图
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
chartRef.current = chart;
|
||||||
|
lineSeriesRef.current = lineSeries;
|
||||||
|
|
||||||
|
// 响应式调整
|
||||||
|
const handleResize = () => {
|
||||||
|
if (chartContainerRef.current && chart) {
|
||||||
|
chart.applyOptions({
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}, [data, metricName]);
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
const handleTimeRangeChange = (range: TimeRange) => {
|
||||||
|
setSelectedRange(range);
|
||||||
|
|
||||||
|
if (!chartRef.current || data.length === 0) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let startDate: Date;
|
||||||
|
|
||||||
|
switch (range) {
|
||||||
|
case '1M':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
||||||
|
break;
|
||||||
|
case '3M':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
|
||||||
|
break;
|
||||||
|
case '6M':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
|
||||||
|
break;
|
||||||
|
case '1Y':
|
||||||
|
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
||||||
|
break;
|
||||||
|
case 'YTD':
|
||||||
|
startDate = new Date(now.getFullYear(), 0, 1); // 当年1月1日
|
||||||
|
break;
|
||||||
|
case 'ALL':
|
||||||
|
default:
|
||||||
|
chartRef.current.timeScale().fitContent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置可见范围
|
||||||
|
const startTimestamp = startDate.getTime() / 1000;
|
||||||
|
const endTimestamp = now.getTime() / 1000;
|
||||||
|
|
||||||
|
chartRef.current.timeScale().setVisibleRange({
|
||||||
|
from: startTimestamp as Time,
|
||||||
|
to: endTimestamp as Time,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置缩放
|
||||||
|
const handleReset = () => {
|
||||||
|
if (chartRef.current) {
|
||||||
|
chartRef.current.timeScale().fitContent();
|
||||||
|
setSelectedRange('ALL');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 截图功能
|
||||||
|
const handleScreenshot = () => {
|
||||||
|
if (!chartRef.current) return;
|
||||||
|
|
||||||
|
const canvas = chartContainerRef.current?.querySelector('canvas');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${metricName}_${new Date().toISOString().split('T')[0]}.png`;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全屏切换
|
||||||
|
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 (
|
||||||
|
<VStack align="stretch" spacing={4} w="100%">
|
||||||
|
{/* 工具栏 */}
|
||||||
|
<Flex justify="space-between" align="center" wrap="wrap" gap={4}>
|
||||||
|
{/* 时间范围选择 */}
|
||||||
|
<ButtonGroup size="sm" isAttached variant="outline">
|
||||||
|
{(['1M', '3M', '6M', '1Y', 'YTD', 'ALL'] as TimeRange[]).map((range) => (
|
||||||
|
<Button
|
||||||
|
key={range}
|
||||||
|
onClick={() => handleTimeRangeChange(range)}
|
||||||
|
bg={selectedRange === range ? themeColors.primary.gold : 'transparent'}
|
||||||
|
color={
|
||||||
|
selectedRange === range ? themeColors.bg.primary : themeColors.text.secondary
|
||||||
|
}
|
||||||
|
borderColor={themeColors.border.gold}
|
||||||
|
_hover={{
|
||||||
|
bg: selectedRange === range ? themeColors.primary.goldLight : themeColors.bg.card,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{range}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
{/* 图表操作 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Tooltip label="重置视图">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
color={themeColors.text.secondary}
|
||||||
|
_hover={{ color: themeColors.primary.gold }}
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
<Icon as={FaRedo} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="截图">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
color={themeColors.text.secondary}
|
||||||
|
_hover={{ color: themeColors.primary.gold }}
|
||||||
|
onClick={handleScreenshot}
|
||||||
|
>
|
||||||
|
<Icon as={FaCamera} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
color={themeColors.text.secondary}
|
||||||
|
_hover={{ color: themeColors.primary.gold }}
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
>
|
||||||
|
<Icon as={isFullscreen ? FaCompress : FaExpand} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 统计数据 */}
|
||||||
|
<Flex
|
||||||
|
justify="space-around"
|
||||||
|
align="center"
|
||||||
|
bg={themeColors.bg.secondary}
|
||||||
|
p={3}
|
||||||
|
borderRadius="md"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={themeColors.border.default}
|
||||||
|
wrap="wrap"
|
||||||
|
gap={4}
|
||||||
|
>
|
||||||
|
<VStack spacing={0}>
|
||||||
|
<Text color={themeColors.text.muted} fontSize="xs">
|
||||||
|
最新值
|
||||||
|
</Text>
|
||||||
|
<Text color={themeColors.text.gold} fontSize="lg" fontWeight="bold">
|
||||||
|
{formatNumber(stats.latest)} {unit}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
color={stats.change >= 0 ? '#00ff88' : '#ff4444'}
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{stats.change >= 0 ? '+' : ''}
|
||||||
|
{formatNumber(stats.change)} ({stats.changePercent.toFixed(2)}%)
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack spacing={0}>
|
||||||
|
<Text color={themeColors.text.muted} fontSize="xs">
|
||||||
|
平均值
|
||||||
|
</Text>
|
||||||
|
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||||
|
{formatNumber(stats.avg)} {unit}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack spacing={0}>
|
||||||
|
<Text color={themeColors.text.muted} fontSize="xs">
|
||||||
|
最高值
|
||||||
|
</Text>
|
||||||
|
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||||
|
{formatNumber(stats.max)} {unit}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack spacing={0}>
|
||||||
|
<Text color={themeColors.text.muted} fontSize="xs">
|
||||||
|
最低值
|
||||||
|
</Text>
|
||||||
|
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||||
|
{formatNumber(stats.min)} {unit}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack spacing={0}>
|
||||||
|
<Text color={themeColors.text.muted} fontSize="xs">
|
||||||
|
数据点数
|
||||||
|
</Text>
|
||||||
|
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||||
|
{data.filter((item) => item.value !== null).length}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack spacing={0}>
|
||||||
|
<Text color={themeColors.text.muted} fontSize="xs">
|
||||||
|
频率
|
||||||
|
</Text>
|
||||||
|
<Text color={themeColors.text.primary} fontSize="md" fontWeight="bold">
|
||||||
|
{frequency}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 图表容器 */}
|
||||||
|
<Box
|
||||||
|
ref={chartContainerRef}
|
||||||
|
w="100%"
|
||||||
|
h="500px"
|
||||||
|
borderRadius="md"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={themeColors.border.gold}
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
bg={themeColors.bg.card}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 提示信息 */}
|
||||||
|
<Flex justify="space-between" align="center" fontSize="xs" color={themeColors.text.muted}>
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<Text>💡 提示:滚动鼠标滚轮缩放,拖拽移动视图</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text>数据来源: {metricName}</Text>
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TradingViewChart;
|
||||||
Reference in New Issue
Block a user