update pay function
This commit is contained in:
509
src/views/DataBrowser/MetricDataModal.tsx
Normal file
509
src/views/DataBrowser/MetricDataModal.tsx
Normal file
@@ -0,0 +1,509 @@
|
||||
import React, { useState, useEffect, useMemo } 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 ReactECharts from 'echarts-for-react';
|
||||
import { fetchMetricData, MetricDataResponse, TreeMetric } from '@services/categoryService';
|
||||
|
||||
// 黑金主题配色
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 准备图表数据
|
||||
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
|
||||
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>
|
||||
{/* 折线图 */}
|
||||
<TabPanel p={4}>
|
||||
{chartOption ? (
|
||||
<Box>
|
||||
<ReactECharts
|
||||
option={chartOption}
|
||||
style={{ height: '500px', width: '100%' }}
|
||||
opts={{ renderer: 'svg' }}
|
||||
/>
|
||||
<Text
|
||||
textAlign="center"
|
||||
color={themeColors.text.muted}
|
||||
fontSize="sm"
|
||||
mt={2}
|
||||
>
|
||||
共 {metricData.data.length} 条数据点
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<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;
|
||||
Reference in New Issue
Block a user