update pay function

This commit is contained in:
2025-11-20 13:25:50 +08:00
parent 80676dd622
commit 8eff6b1a95
4 changed files with 733 additions and 69 deletions

View File

@@ -30,8 +30,8 @@
"tags": [
"分类树"
],
"summary": "获取完整分类树",
"description": "获取指定数据源的完整分类树状结构。\n\n## 使用场景\n- 前端树形组件初始化\n- 构建完整的分类导航\n- 级联选择器数据源\n\n## 注意事项\n- SMM树约53MB,Mysteel约152MB\n- 建议前端实现懒加载或缓存策略\n- 响应时间取决于网络带宽\n",
"summary": "获取分类树(支持深度控制)",
"description": "获取指定数据源的分类树状结构,支持深度控制。\n\n## 使用场景\n- 前端树形组件初始化(默认只加载第一层)\n- 懒加载:用户展开时再加载下一层\n- 级联选择器数据源\n\n## 默认行为\n- **默认只返回第一层** (max_depth=1),大幅减少数据传输量\n- SMM第一层约43个节点,Mysteel第一层约2个节点\n- 完整树数据量: SMM约53MB, Mysteel约152MB\n\n## 推荐用法\n1. 首次加载:不传max_depth(默认1层)\n2. 用户点击节点:调用 /api/category-tree/node 获取子节点\n",
"operationId": "getCategoryTree",
"parameters": [
{
@@ -47,6 +47,19 @@
]
},
"example": "SMM"
},
{
"name": "max_depth",
"in": "query",
"description": "返回的最大层级深度\n- 1: 只返回第一层(默认,推荐)\n- 2: 返回前两层\n- 999: 返回完整树(不推荐,数据量大)\n",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 1
},
"example": 1
}
],
"responses": {
@@ -268,7 +281,8 @@
"required": [
"name",
"path",
"level"
"level",
"has_children"
],
"properties": {
"name": {
@@ -287,9 +301,14 @@
"minimum": 1,
"example": 3
},
"has_children": {
"type": "boolean",
"description": "是否有子节点(用于前端判断是否可展开)",
"example": true
},
"children": {
"type": "array",
"description": "子节点列表",
"description": "子节点列表(根据max_depth可能为空数组)",
"items": {
"$ref": "#/components/schemas/TreeNode"
}

View File

@@ -34,21 +34,41 @@ export interface ErrorResponse {
detail: string;
}
export interface MetricDataPoint {
date: string;
value: number | null;
}
export interface MetricDataResponse {
metric_id: string;
metric_name: string;
source: string;
frequency: string;
unit: string;
data: MetricDataPoint[];
total_count: number;
}
/**
* 获取完整分类树
* 获取分类树(支持深度控制)
* @param source 数据源类型 ('SMM' | 'Mysteel')
* @returns 完整的分类树数据
* @param maxDepth 返回的最大层级深度默认1层推荐懒加载
* @returns 分类树数据
*/
export const fetchCategoryTree = async (
source: 'SMM' | 'Mysteel'
source: 'SMM' | 'Mysteel',
maxDepth: number = 1
): Promise<CategoryTreeResponse> => {
try {
const response = await fetch(`/category-api/api/category-tree?source=${source}`, {
const response = await fetch(
`/category-api/api/category-tree?source=${source}&max_depth=${maxDepth}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
}
);
if (!response.ok) {
const errorData: ErrorResponse = await response.json();
@@ -190,3 +210,46 @@ export const getParentPaths = (path: string): string[] => {
return parentPaths;
};
/**
* 获取指标数据详情
* @param metricId 指标ID
* @param startDate 开始日期可选格式YYYY-MM-DD
* @param endDate 结束日期可选格式YYYY-MM-DD
* @param limit 返回数据条数可选默认100
* @returns 指标数据
*/
export const fetchMetricData = async (
metricId: string,
startDate?: string,
endDate?: string,
limit: number = 100
): Promise<MetricDataResponse> => {
try {
const params = new URLSearchParams({
metric_id: metricId,
limit: limit.toString(),
});
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
const response = await fetch(`/category-api/api/metric-data?${params.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData: ErrorResponse = await response.json();
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
const data: MetricDataResponse = await response.json();
return data;
} catch (error) {
console.error('fetchMetricData error:', error);
throw error;
}
};

View 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;

View File

@@ -19,7 +19,7 @@ import {
CardBody,
Divider,
SimpleGrid,
Collapse,
useDisclosure,
} from '@chakra-ui/react';
import {
FaDatabase,
@@ -30,12 +30,12 @@ import {
FaHome,
FaChevronRight,
FaChevronDown,
FaChevronUp,
FaFilter,
FaTimes,
FaEye,
} from 'react-icons/fa';
import { motion } from 'framer-motion';
import { fetchCategoryTree, fetchCategoryNode } from '@services/categoryService';
import { fetchCategoryTree, fetchCategoryNode, TreeNode, TreeMetric, CategoryTreeResponse } from '@services/categoryService';
import MetricDataModal from './MetricDataModal';
// 黑金主题配色
const themeColors = {
@@ -68,38 +68,18 @@ const themeColors = {
const MotionBox = motion(Box);
const MotionCard = motion(Card);
interface TreeNode {
name: string;
path: string;
level: number;
children?: TreeNode[];
metrics?: TreeMetric[];
}
interface TreeMetric {
metric_id: string;
metric_name: string;
source: string;
frequency: string;
unit: string;
description?: string;
}
interface CategoryTreeResponse {
source: string;
total_metrics: number;
tree: TreeNode[];
}
// 树节点组件
// 树节点组件(支持懒加载)
const TreeNodeComponent: React.FC<{
node: TreeNode;
source: 'SMM' | 'Mysteel';
onNodeClick: (node: TreeNode) => void;
expandedNodes: Set<string>;
onToggleExpand: (path: string) => void;
onToggleExpand: (node: TreeNode) => Promise<void>;
searchQuery: string;
}> = ({ node, onNodeClick, expandedNodes, onToggleExpand, searchQuery }) => {
loadingNodes: Set<string>;
}> = ({ node, source, onNodeClick, expandedNodes, onToggleExpand, searchQuery, loadingNodes }) => {
const isExpanded = expandedNodes.has(node.path);
const isLoading = loadingNodes.has(node.path);
const hasChildren = node.children && node.children.length > 0;
const hasMetrics = node.metrics && node.metrics.length > 0;
@@ -130,13 +110,13 @@ const TreeNodeComponent: React.FC<{
borderRadius="md"
transition="all 0.2s"
onClick={() => {
if (hasChildren) {
onToggleExpand(node.path);
}
onToggleExpand(node);
onNodeClick(node);
}}
>
{hasChildren ? (
{isLoading ? (
<Spinner size="xs" color={themeColors.primary.gold} mr={2} />
) : hasChildren || !hasMetrics ? (
<Icon
as={isExpanded ? FaChevronDown : FaChevronRight}
color={themeColors.text.muted}
@@ -148,8 +128,8 @@ const TreeNodeComponent: React.FC<{
)}
<Icon
as={hasChildren ? (isExpanded ? FaFolderOpen : FaFolder) : FaFile}
color={hasChildren ? themeColors.primary.gold : themeColors.text.secondary}
as={hasChildren || !hasMetrics ? (isExpanded ? FaFolderOpen : FaFolder) : FaFile}
color={hasChildren || !hasMetrics ? themeColors.primary.gold : themeColors.text.secondary}
mr={2}
/>
@@ -175,10 +155,12 @@ const TreeNodeComponent: React.FC<{
<TreeNodeComponent
key={child.path}
node={child}
source={source}
onNodeClick={onNodeClick}
expandedNodes={expandedNodes}
onToggleExpand={onToggleExpand}
searchQuery={searchQuery}
loadingNodes={loadingNodes}
/>
))}
</Box>
@@ -187,8 +169,8 @@ const TreeNodeComponent: React.FC<{
);
};
// 指标卡片组件
const MetricCard: React.FC<{ metric: TreeMetric }> = ({ metric }) => {
// 指标卡片组件(可点击查看详情)
const MetricCard: React.FC<{ metric: TreeMetric; onClick: () => void }> = ({ metric, onClick }) => {
return (
<MotionCard
bg={themeColors.bg.card}
@@ -196,6 +178,8 @@ const MetricCard: React.FC<{ metric: TreeMetric }> = ({ metric }) => {
borderColor={themeColors.border.default}
borderRadius="lg"
overflow="hidden"
cursor="pointer"
onClick={onClick}
whileHover={{
borderColor: themeColors.border.goldGlow,
scale: 1.02,
@@ -205,7 +189,7 @@ const MetricCard: React.FC<{ metric: TreeMetric }> = ({ metric }) => {
<CardBody>
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text color={themeColors.text.primary} fontWeight="bold" fontSize="sm">
<Text color={themeColors.text.primary} fontWeight="bold" fontSize="sm" flex="1">
{metric.metric_name}
</Text>
<Badge
@@ -244,9 +228,20 @@ const MetricCard: React.FC<{ metric: TreeMetric }> = ({ metric }) => {
</Text>
)}
<HStack justify="space-between">
<Text color={themeColors.text.muted} fontSize="xs" fontFamily="monospace">
ID: {metric.metric_id}
</Text>
<Button
size="xs"
variant="ghost"
color={themeColors.primary.gold}
leftIcon={<FaEye />}
_hover={{ bg: themeColors.bg.cardHover }}
>
</Button>
</HStack>
</VStack>
</CardBody>
</MotionCard>
@@ -261,11 +256,13 @@ const DataBrowser: React.FC = () => {
const [currentNode, setCurrentNode] = useState<TreeNode | null>(null);
const [breadcrumbs, setBreadcrumbs] = useState<string[]>([]);
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
const [showFilters, setShowFilters] = useState(false);
const [loadingNodes, setLoadingNodes] = useState<Set<string>>(new Set());
const [selectedMetric, setSelectedMetric] = useState<TreeMetric | null>(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast();
// 加载分类树
// 加载分类树(只加载第一层)
useEffect(() => {
loadCategoryTree();
}, [selectedSource]);
@@ -273,10 +270,11 @@ const DataBrowser: React.FC = () => {
const loadCategoryTree = async () => {
setLoading(true);
try {
const data = await fetchCategoryTree(selectedSource);
const data = await fetchCategoryTree(selectedSource, 1); // 只加载第一层
setTreeData(data);
setCurrentNode(null);
setBreadcrumbs([]);
setExpandedNodes(new Set());
} catch (error) {
toast({
title: '加载失败',
@@ -290,17 +288,75 @@ const DataBrowser: React.FC = () => {
}
};
// 切换节点展开状态
const toggleNodeExpand = (path: string) => {
// 切换节点展开状态(懒加载子节点)
const toggleNodeExpand = async (node: TreeNode) => {
const isCurrentlyExpanded = expandedNodes.has(node.path);
if (isCurrentlyExpanded) {
// 收起节点
setExpandedNodes((prev) => {
const newSet = new Set(prev);
if (newSet.has(path)) {
newSet.delete(path);
} else {
newSet.add(path);
}
newSet.delete(node.path);
return newSet;
});
} else {
// 展开节点 - 检查是否需要加载子节点
const needsLoading = !node.children || node.children.length === 0;
if (needsLoading) {
// 添加加载状态
setLoadingNodes((prev) => new Set(prev).add(node.path));
try {
// 从服务器加载子节点
const nodeData = await fetchCategoryNode(node.path, selectedSource);
// 更新树数据
setTreeData((prevData) => {
if (!prevData) return prevData;
const updateNode = (nodes: TreeNode[]): TreeNode[] => {
return nodes.map((n) => {
if (n.path === node.path) {
return { ...n, children: nodeData.children, metrics: nodeData.metrics };
}
if (n.children) {
return { ...n, children: updateNode(n.children) };
}
return n;
});
};
return {
...prevData,
tree: updateNode(prevData.tree),
};
});
// 更新当前节点(如果是当前选中的节点)
if (currentNode && currentNode.path === node.path) {
setCurrentNode(nodeData);
}
} catch (error) {
toast({
title: '加载失败',
description: '无法加载子节点数据',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoadingNodes((prev) => {
const newSet = new Set(prev);
newSet.delete(node.path);
return newSet;
});
}
}
// 展开节点
setExpandedNodes((prev) => new Set(prev).add(node.path));
}
};
// 处理节点点击
@@ -339,6 +395,12 @@ const DataBrowser: React.FC = () => {
}
};
// 处理指标点击
const handleMetricClick = (metric: TreeMetric) => {
setSelectedMetric(metric);
onOpen();
};
// 过滤树节点(根据搜索关键词)
const filteredTree = useMemo(() => {
if (!treeData || !searchQuery) return treeData?.tree || [];
@@ -572,10 +634,12 @@ const DataBrowser: React.FC = () => {
<TreeNodeComponent
key={node.path}
node={node}
source={selectedSource}
onNodeClick={handleNodeClick}
expandedNodes={expandedNodes}
onToggleExpand={toggleNodeExpand}
searchQuery={searchQuery}
loadingNodes={loadingNodes}
/>
))}
</VStack>
@@ -627,7 +691,11 @@ const DataBrowser: React.FC = () => {
{currentNode.metrics && currentNode.metrics.length > 0 ? (
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4} mt={4}>
{currentNode.metrics.map((metric) => (
<MetricCard key={metric.metric_id} metric={metric} />
<MetricCard
key={metric.metric_id}
metric={metric}
onClick={() => handleMetricClick(metric)}
/>
))}
</SimpleGrid>
) : (
@@ -663,6 +731,11 @@ const DataBrowser: React.FC = () => {
</MotionBox>
</Flex>
</Container>
{/* 指标数据详情模态框 */}
{selectedMetric && (
<MetricDataModal isOpen={isOpen} onClose={onClose} metric={selectedMetric} />
)}
</Box>
);
};