update pay function

This commit is contained in:
2025-11-20 13:43:04 +08:00
parent 8eff6b1a95
commit 03aee75235
3 changed files with 709 additions and 75 deletions

View File

@@ -34,7 +34,16 @@ import {
FaEye,
} from 'react-icons/fa';
import { motion } from 'framer-motion';
import { fetchCategoryTree, fetchCategoryNode, TreeNode, TreeMetric, CategoryTreeResponse } from '@services/categoryService';
import {
fetchCategoryTree,
fetchCategoryNode,
searchMetrics,
TreeNode,
TreeMetric,
CategoryTreeResponse,
MetricSearchResult,
SearchResponse
} from '@services/categoryService';
import MetricDataModal from './MetricDataModal';
// 黑金主题配色
@@ -253,6 +262,8 @@ const DataBrowser: React.FC = () => {
const [treeData, setTreeData] = useState<CategoryTreeResponse | null>(null);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResponse | null>(null);
const [searching, setSearching] = useState(false);
const [currentNode, setCurrentNode] = useState<TreeNode | null>(null);
const [breadcrumbs, setBreadcrumbs] = useState<string[]>([]);
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
@@ -275,6 +286,7 @@ const DataBrowser: React.FC = () => {
setCurrentNode(null);
setBreadcrumbs([]);
setExpandedNodes(new Set());
setSearchResults(null); // 清空搜索结果
} catch (error) {
toast({
title: '加载失败',
@@ -288,6 +300,43 @@ const DataBrowser: React.FC = () => {
}
};
// 执行搜索
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);
@@ -401,28 +450,12 @@ const DataBrowser: React.FC = () => {
onOpen();
};
// 过滤树节点(根据搜索关键词
const filteredTree = useMemo(() => {
if (!treeData || !searchQuery) return treeData?.tree || [];
const filterNodes = (nodes: TreeNode[]): TreeNode[] => {
return nodes
.map((node) => {
const matchesName = node.name.toLowerCase().includes(searchQuery.toLowerCase());
const filteredChildren = node.children ? filterNodes(node.children) : [];
if (matchesName || filteredChildren.length > 0) {
return {
...node,
children: filteredChildren,
};
}
return null;
})
.filter(Boolean) as TreeNode[];
};
return filterNodes(treeData.tree);
// 显示的树节点(搜索时不显示树
const displayTree = useMemo(() => {
if (searchQuery.trim()) {
return []; // 搜索时不显示树
}
return treeData?.tree || [];
}, [treeData, searchQuery]);
return (
@@ -518,40 +551,64 @@ const DataBrowser: React.FC = () => {
mb={6}
>
<CardBody>
<HStack spacing={4}>
<Input
placeholder="搜索分类或指标名称..."
value={searchQuery}
onChange={(e) => 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}`,
}}
/>
<Button
leftIcon={<FaSearch />}
bg={themeColors.primary.gold}
color={themeColors.bg.primary}
_hover={{ bg: themeColors.primary.goldLight }}
>
</Button>
{searchQuery && (
<VStack spacing={3} align="stretch">
<HStack spacing={4}>
<Input
placeholder="搜索分类或指标名称..."
value={searchQuery}
onChange={(e) => 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}`,
}}
/>
<Button
leftIcon={<FaTimes />}
variant="ghost"
color={themeColors.text.secondary}
_hover={{ color: themeColors.text.primary }}
onClick={() => setSearchQuery('')}
leftIcon={<FaSearch />}
bg={themeColors.primary.gold}
color={themeColors.bg.primary}
_hover={{ bg: themeColors.primary.goldLight }}
onClick={handleSearch}
isLoading={searching}
>
</Button>
{searchQuery && (
<Button
leftIcon={<FaTimes />}
variant="ghost"
color={themeColors.text.secondary}
_hover={{ color: themeColors.text.primary }}
onClick={() => setSearchQuery('')}
>
</Button>
)}
</HStack>
{/* 搜索结果提示 */}
{searchResults && (
<Flex align="center" justify="space-between" py={2}>
<Text color={themeColors.text.secondary} fontSize="sm">
<Text as="span" color={themeColors.primary.gold} fontWeight="bold">{searchResults.total}</Text>
</Text>
<Text color={themeColors.text.muted} fontSize="xs">
: "{searchResults.query}"
</Text>
</Flex>
)}
</HStack>
{searching && (
<Flex align="center" justify="center" py={2}>
<Spinner size="sm" color={themeColors.primary.gold} mr={2} />
<Text color={themeColors.text.secondary} fontSize="sm">
...
</Text>
</Flex>
)}
</VStack>
</CardBody>
</Card>
</MotionBox>
@@ -628,9 +685,77 @@ const DataBrowser: React.FC = () => {
<Flex justify="center" align="center" py={10}>
<Spinner color={themeColors.primary.gold} size="xl" />
</Flex>
) : (
) : searchQuery.trim() ? (
// 搜索模式:显示搜索结果列表
<VStack align="stretch" spacing={1}>
{filteredTree.map((node) => (
{searchResults && searchResults.results.length > 0 ? (
searchResults.results.map((result) => (
<Box
key={result.metric_id}
p={3}
cursor="pointer"
bg="transparent"
_hover={{ bg: themeColors.bg.cardHover }}
borderRadius="md"
borderLeftWidth="3px"
borderLeftColor="transparent"
_hover={{ borderLeftColor: themeColors.primary.gold }}
transition="all 0.2s"
onClick={() => {
// 转换搜索结果为 TreeMetric 格式
const metric: TreeMetric = {
metric_id: result.metric_id,
metric_name: result.metric_name,
source: result.source,
frequency: result.frequency,
unit: result.unit,
description: result.description,
};
handleMetricClick(metric);
}}
>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Text color={themeColors.text.primary} fontSize="sm" fontWeight="bold" flex="1">
{result.metric_name}
</Text>
<Badge
bg={result.source === 'SMM' ? 'blue.500' : 'green.500'}
color="white"
fontSize="xs"
>
{result.source}
</Badge>
</HStack>
<HStack spacing={4} fontSize="xs" color={themeColors.text.muted}>
<Text>: {result.category_path}</Text>
<Text>: {result.frequency}</Text>
<Text>: {result.unit || '-'}</Text>
</HStack>
{result.score && (
<Text fontSize="xs" color={themeColors.text.muted}>
: {(result.score * 100).toFixed(0)}%
</Text>
)}
</VStack>
</Box>
))
) : searchResults ? (
<Flex justify="center" align="center" py={10}>
<VStack spacing={3}>
<Icon as={FaSearch} color={themeColors.text.muted} boxSize={12} />
<Text color={themeColors.text.muted}></Text>
<Text color={themeColors.text.muted} fontSize="sm">
使
</Text>
</VStack>
</Flex>
) : null}
</VStack>
) : (
// 正常模式:显示分类树
<VStack align="stretch" spacing={1}>
{displayTree.map((node) => (
<TreeNodeComponent
key={node.path}
node={node}
@@ -638,7 +763,7 @@ const DataBrowser: React.FC = () => {
onNodeClick={handleNodeClick}
expandedNodes={expandedNodes}
onToggleExpand={toggleNodeExpand}
searchQuery={searchQuery}
searchQuery=""
loadingNodes={loadingNodes}
/>
))}