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

@@ -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>
);
};