import React, { useState, useEffect, useMemo } from 'react'; import { Box, Container, Flex, Text, Input, Button, VStack, HStack, Badge, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Icon, Spinner, useToast, Card, CardBody, Divider, SimpleGrid, useDisclosure, } from '@chakra-ui/react'; import { FaDatabase, FaFolder, FaFolderOpen, FaFile, FaSearch, FaHome, FaChevronRight, FaChevronDown, FaTimes, FaEye, } from 'react-icons/fa'; import { motion } from 'framer-motion'; import { fetchCategoryTree, fetchCategoryNode, searchMetrics, TreeNode, TreeMetric, CategoryTreeResponse, MetricSearchResult, SearchResponse } from '@services/categoryService'; import MetricDataModal from './MetricDataModal'; // 黑金主题配色 const themeColors = { bgGradient: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%)', bgRadialGold: 'radial-gradient(circle at center, rgba(212, 175, 55, 0.1) 0%, transparent 70%)', primary: { gold: '#D4AF37', goldLight: '#F4E3A7', goldDark: '#B8941F', }, 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)', }, }; const MotionBox = motion(Box); const MotionCard = motion(Card); // 树节点组件(支持懒加载) const TreeNodeComponent: React.FC<{ node: TreeNode; source: 'SMM' | 'Mysteel'; onNodeClick: (node: TreeNode) => void; expandedNodes: Set; onToggleExpand: (node: TreeNode) => Promise; searchQuery: string; loadingNodes: Set; }> = ({ 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; // 高亮搜索关键词 const highlightText = (text: string) => { if (!searchQuery) return text; const parts = text.split(new RegExp(`(${searchQuery})`, 'gi')); return parts.map((part, index) => part.toLowerCase() === searchQuery.toLowerCase() ? ( {part} ) : ( part ) ); }; return ( { onToggleExpand(node); onNodeClick(node); }} > {isLoading ? ( ) : hasChildren || !hasMetrics ? ( ) : ( )} {highlightText(node.name)} {hasMetrics && ( {node.metrics.length} )} {isExpanded && hasChildren && ( {node.children!.map((child) => ( ))} )} ); }; // 指标卡片组件(可点击查看详情) const MetricCard: React.FC<{ metric: TreeMetric; onClick: () => void }> = ({ metric, onClick }) => { return ( {metric.metric_name} {metric.source} 频率 {metric.frequency} 单位 {metric.unit || '-'} {metric.description && ( {metric.description} )} ID: {metric.metric_id} ); }; const DataBrowser: React.FC = () => { const [selectedSource, setSelectedSource] = useState<'SMM' | 'Mysteel'>('SMM'); const [treeData, setTreeData] = useState(null); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); const [searching, setSearching] = useState(false); const [currentNode, setCurrentNode] = useState(null); const [breadcrumbs, setBreadcrumbs] = useState([]); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [loadingNodes, setLoadingNodes] = useState>(new Set()); const [selectedMetric, setSelectedMetric] = useState(null); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); // 加载分类树(只加载第一层) useEffect(() => { loadCategoryTree(); }, [selectedSource]); const loadCategoryTree = async () => { setLoading(true); try { const data = await fetchCategoryTree(selectedSource, 1); // 只加载第一层 setTreeData(data); setCurrentNode(null); setBreadcrumbs([]); setExpandedNodes(new Set()); setSearchResults(null); // 清空搜索结果 } catch (error) { toast({ title: '加载失败', description: '无法加载分类树数据', status: 'error', duration: 3000, isClosable: true, }); } finally { setLoading(false); } }; // 执行搜索 const handleSearch = async () => { if (!searchQuery.trim()) { setSearchResults(null); return; } setSearching(true); try { const results = await searchMetrics(searchQuery, selectedSource, undefined, 100); setSearchResults(results); } catch (error) { toast({ title: '搜索失败', description: '无法搜索指标数据', status: 'error', duration: 3000, isClosable: true, }); } finally { setSearching(false); } }; // 当搜索关键词变化时,自动搜索 useEffect(() => { const timer = setTimeout(() => { if (searchQuery.trim()) { handleSearch(); } else { setSearchResults(null); } }, 500); // 防抖 500ms return () => clearTimeout(timer); }, [searchQuery, selectedSource]); // 切换节点展开状态(懒加载子节点) 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 hasChildren = node.children && node.children.length > 0; // 如果没有子节点数据,尝试从服务器加载 if (!hasChildren) { // 添加加载状态 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)); } }; // 处理节点点击 const handleNodeClick = (node: TreeNode) => { setCurrentNode(node); const pathParts = node.path.split('|'); setBreadcrumbs(pathParts); }; // 处理面包屑导航 const handleBreadcrumbClick = (index: number) => { if (index === -1) { setCurrentNode(null); setBreadcrumbs([]); return; } const targetPath = breadcrumbs.slice(0, index + 1).join('|'); // 在树中查找对应节点 const findNode = (nodes: TreeNode[], path: string): TreeNode | null => { for (const node of nodes) { if (node.path === path) return node; if (node.children) { const found = findNode(node.children, path); if (found) return found; } } return null; }; if (treeData) { const node = findNode(treeData.tree, targetPath); if (node) { handleNodeClick(node); } } }; // 处理指标点击 const handleMetricClick = (metric: TreeMetric) => { setSelectedMetric(metric); onOpen(); }; // 显示的树节点(搜索时不显示树) const displayTree = useMemo(() => { if (searchQuery.trim()) { return []; // 搜索时不显示树 } return treeData?.tree || []; }, [treeData, searchQuery]); return ( {/* 金色光晕背景 */} {/* 标题区域 */} 数据浏览器 化工商品数据分类树 - 探索海量行业指标 {/* 数据源切换 */} {/* 搜索和过滤 */} setSearchQuery(e.target.value)} bg={themeColors.bg.secondary} borderColor={themeColors.border.default} color={themeColors.text.primary} _placeholder={{ color: themeColors.text.muted }} _focus={{ borderColor: themeColors.primary.gold, boxShadow: `0 0 0 1px ${themeColors.primary.gold}`, }} /> {searchQuery && ( )} {/* 搜索结果提示 */} {searchResults && ( 找到 {searchResults.total} 个相关指标 关键词: "{searchResults.query}" )} {searching && ( 搜索中... )} {/* 面包屑导航 */} {breadcrumbs.length > 0 && ( } > handleBreadcrumbClick(-1)} > {breadcrumbs.map((crumb, index) => ( handleBreadcrumbClick(index)} > {crumb} ))} )} {/* 主内容区域 */} {/* 左侧:分类树 */} {loading ? ( ) : searchQuery.trim() ? ( // 搜索模式:显示搜索结果列表 {searchResults && searchResults.results.length > 0 ? ( searchResults.results.map((result) => ( { // 转换搜索结果为 TreeMetric 格式 const metric: TreeMetric = { metric_id: result.metric_id, metric_name: result.metric_name, source: result.source as 'SMM' | 'Mysteel', frequency: result.frequency, unit: result.unit, description: result.description, }; // 更新面包屑导航为搜索结果的路径 const pathParts = result.category_path.split(' > '); setBreadcrumbs(pathParts); // 清空搜索框,显示树结构 setSearchQuery(''); handleMetricClick(metric); }} > {result.metric_name} {result.source} 路径: {result.category_path} 频率: {result.frequency} 单位: {result.unit || '-'} {result.score && ( 相关度: {(result.score * 100).toFixed(0)}% )} )) ) : searchResults ? ( 未找到匹配的指标 尝试使用不同的关键词 ) : null} ) : ( // 正常模式:显示分类树 {displayTree.map((node) => ( ))} )} {/* 右侧:指标详情 */} {currentNode ? ( {currentNode.name} 层级 {currentNode.level} | 路径: {currentNode.path} {currentNode.metrics && currentNode.metrics.length > 0 && ( {currentNode.metrics.length} 个指标 )} {currentNode.metrics && currentNode.metrics.length > 0 ? ( {currentNode.metrics.map((metric) => ( handleMetricClick(metric)} /> ))} ) : ( {currentNode.children && currentNode.children.length > 0 ? '该节点包含子分类,请展开查看' : '该节点暂无指标数据'} )} ) : ( 选择左侧分类树节点查看详情 {treeData && ( 当前数据源共有 {treeData.total_metrics.toLocaleString()} 个指标 )} )} {/* 指标数据详情模态框 */} {selectedMetric && ( )} ); }; export default DataBrowser;