update pay function
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
<Text color={themeColors.text.muted} fontSize="xs" fontFamily="monospace">
|
||||
ID: {metric.metric_id}
|
||||
</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) => {
|
||||
setExpandedNodes((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(path)) {
|
||||
newSet.delete(path);
|
||||
} else {
|
||||
newSet.add(path);
|
||||
// 切换节点展开状态(懒加载子节点)
|
||||
const toggleNodeExpand = async (node: TreeNode) => {
|
||||
const isCurrentlyExpanded = expandedNodes.has(node.path);
|
||||
|
||||
if (isCurrentlyExpanded) {
|
||||
// 收起节点
|
||||
setExpandedNodes((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user