Files
vf_react/src/views/DataBrowser/MetricDataModal.tsx
2025-11-20 16:19:52 +08:00

377 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
Box,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Text,
HStack,
VStack,
Badge,
Spinner,
Flex,
Icon,
Button,
Input,
useToast,
} from '@chakra-ui/react';
import { FaTable, FaChartLine, FaCalendar, FaDownload } from 'react-icons/fa';
import { fetchMetricData, MetricDataResponse, TreeMetric } from '@services/categoryService';
import SimpleLineChart from './SimpleLineChart';
// 黑金主题配色
const themeColors = {
bg: {
primary: '#0a0a0a',
secondary: '#1a1a1a',
card: '#1e1e1e',
cardHover: '#252525',
},
text: {
primary: '#ffffff',
secondary: '#b8b8b8',
muted: '#808080',
gold: '#D4AF37',
},
border: {
default: 'rgba(255, 255, 255, 0.1)',
gold: 'rgba(212, 175, 55, 0.3)',
goldGlow: 'rgba(212, 175, 55, 0.5)',
},
primary: {
gold: '#D4AF37',
goldLight: '#F4E3A7',
goldDark: '#B8941F',
},
};
interface MetricDataModalProps {
isOpen: boolean;
onClose: () => void;
metric: TreeMetric;
}
const MetricDataModal: React.FC<MetricDataModalProps> = ({ isOpen, onClose, metric }) => {
const [loading, setLoading] = useState(false);
const [metricData, setMetricData] = useState<MetricDataResponse | null>(null);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [limit, setLimit] = useState(100);
const toast = useToast();
// 加载数据
useEffect(() => {
if (isOpen && metric) {
loadMetricData();
}
}, [isOpen, metric]);
const loadMetricData = async () => {
setLoading(true);
try {
const data = await fetchMetricData(metric.metric_id, startDate, endDate, limit);
setMetricData(data);
} catch (error) {
toast({
title: '加载失败',
description: '无法加载指标数据',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
};
// 数据已经在 metricData 中,直接传递给 TradingViewChart
// 导出CSV
const handleExportCSV = () => {
if (!metricData || !metricData.data) return;
const csvContent = [
['日期', '数值', '单位'].join(','),
...metricData.data.map((item) => [item.date, item.value ?? '', metricData.unit || ''].join(',')),
].join('\n');
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `${metricData.metric_name}_${Date.now()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast({
title: '导出成功',
description: 'CSV 文件已下载',
status: 'success',
duration: 2000,
});
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="6xl" isCentered>
<ModalOverlay bg="blackAlpha.800" />
<ModalContent
bg={themeColors.bg.card}
borderWidth="1px"
borderColor={themeColors.border.gold}
maxH="90vh"
>
<ModalHeader
bg={themeColors.bg.secondary}
borderBottomWidth="1px"
borderBottomColor={themeColors.border.gold}
>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Text color={themeColors.text.gold} fontSize="lg" fontWeight="bold">
{metric.metric_name}
</Text>
<HStack spacing={2}>
<Badge bg={metric.source === 'SMM' ? 'blue.500' : 'green.500'} color="white">
{metric.source}
</Badge>
<Badge bg={themeColors.border.gold} color={themeColors.primary.gold}>
{metric.frequency}
</Badge>
</HStack>
</HStack>
<HStack spacing={4} fontSize="sm" color={themeColors.text.secondary}>
<Text>ID: {metric.metric_id}</Text>
{metric.unit && <Text>: {metric.unit}</Text>}
</HStack>
</VStack>
</ModalHeader>
<ModalCloseButton color={themeColors.text.secondary} />
<ModalBody p={0}>
{loading ? (
<Flex justify="center" align="center" py={20}>
<VStack spacing={4}>
<Spinner size="xl" color={themeColors.primary.gold} thickness="4px" />
<Text color={themeColors.text.secondary}>...</Text>
</VStack>
</Flex>
) : (
<>
{/* 筛选工具栏 */}
<Box
p={4}
bg={themeColors.bg.secondary}
borderBottomWidth="1px"
borderBottomColor={themeColors.border.default}
>
<HStack spacing={4} wrap="wrap">
<HStack flex="1" minW="200px">
<Icon as={FaCalendar} color={themeColors.text.muted} />
<Input
type="date"
size="sm"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
bg={themeColors.bg.card}
borderColor={themeColors.border.default}
color={themeColors.text.primary}
_focus={{ borderColor: themeColors.primary.gold }}
/>
<Text color={themeColors.text.muted}></Text>
<Input
type="date"
size="sm"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
bg={themeColors.bg.card}
borderColor={themeColors.border.default}
color={themeColors.text.primary}
_focus={{ borderColor: themeColors.primary.gold }}
/>
</HStack>
<HStack>
<Text color={themeColors.text.muted} fontSize="sm">
:
</Text>
<Input
type="number"
size="sm"
w="100px"
value={limit}
onChange={(e) => setLimit(parseInt(e.target.value) || 100)}
bg={themeColors.bg.card}
borderColor={themeColors.border.default}
color={themeColors.text.primary}
_focus={{ borderColor: themeColors.primary.gold }}
/>
</HStack>
<Button
size="sm"
bg={themeColors.primary.gold}
color={themeColors.bg.primary}
_hover={{ bg: themeColors.primary.goldLight }}
onClick={loadMetricData}
>
</Button>
<Button
size="sm"
variant="outline"
borderColor={themeColors.border.gold}
color={themeColors.text.gold}
leftIcon={<FaDownload />}
onClick={handleExportCSV}
isDisabled={!metricData || !metricData.data || metricData.data.length === 0}
>
CSV
</Button>
</HStack>
</Box>
{/* 数据展示 */}
{metricData && (
<Tabs
colorScheme="yellow"
variant="enclosed"
bg={themeColors.bg.primary}
>
<TabList borderBottomColor={themeColors.border.default}>
<Tab
color={themeColors.text.secondary}
_selected={{
color: themeColors.text.gold,
borderColor: themeColors.border.gold,
bg: themeColors.bg.card,
}}
>
<Icon as={FaChartLine} mr={2} />
线
</Tab>
<Tab
color={themeColors.text.secondary}
_selected={{
color: themeColors.text.gold,
borderColor: themeColors.border.gold,
bg: themeColors.bg.card,
}}
>
<Icon as={FaTable} mr={2} />
</Tab>
</TabList>
<TabPanels>
{/* 折线图 - 使用简单 Canvas 实现 */}
<TabPanel p={4}>
{metricData && metricData.data.length > 0 ? (
<SimpleLineChart
data={metricData.data}
metricName={metricData.metric_name}
unit={metricData.unit}
frequency={metricData.frequency}
/>
) : (
<Flex justify="center" align="center" py={20}>
<Text color={themeColors.text.muted}></Text>
</Flex>
)}
</TabPanel>
{/* 数据表格 */}
<TabPanel p={0}>
<Box maxH="500px" overflowY="auto">
<Table variant="simple" size="sm">
<Thead
position="sticky"
top={0}
bg={themeColors.bg.secondary}
zIndex={1}
>
<Tr>
<Th
color={themeColors.text.gold}
borderColor={themeColors.border.default}
>
</Th>
<Th
color={themeColors.text.gold}
borderColor={themeColors.border.default}
>
</Th>
<Th
color={themeColors.text.gold}
borderColor={themeColors.border.default}
isNumeric
>
{metricData.unit && `(${metricData.unit})`}
</Th>
</Tr>
</Thead>
<Tbody>
{metricData.data.map((item, index) => (
<Tr
key={index}
_hover={{ bg: themeColors.bg.cardHover }}
>
<Td
color={themeColors.text.muted}
borderColor={themeColors.border.default}
>
{index + 1}
</Td>
<Td
color={themeColors.text.secondary}
borderColor={themeColors.border.default}
>
{item.date}
</Td>
<Td
color={themeColors.text.primary}
borderColor={themeColors.border.default}
isNumeric
fontWeight="bold"
>
{item.value !== null ? item.value.toLocaleString() : '-'}
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
{metricData.data.length === 0 && (
<Flex justify="center" align="center" py={20}>
<Text color={themeColors.text.muted}></Text>
</Flex>
)}
</TabPanel>
</TabPanels>
</Tabs>
)}
</>
)}
</ModalBody>
</ModalContent>
</Modal>
);
};
export default MetricDataModal;