377 lines
13 KiB
TypeScript
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;
|