feat(Concept): 统一图表容器与共享导航状态

- 使用 ChartContainer 包裹矩形树图和层级图
- 添加 chartDrillPath 共享状态,Tab 切换时导航保持
- 矩形树图/层级图使用不同 contentTopPadding 配置

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-08 19:04:16 +08:00
parent 0a4f068593
commit d08b6f1725

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom'; import { useSearchParams, useNavigate } from 'react-router-dom';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { getConceptHtmlUrl } from '../../utils/textUtils'; import { getConceptHtmlUrl } from '../../utils/textUtils';
@@ -116,6 +116,7 @@ import {
Network, Network,
TrendingUp, TrendingUp,
Zap, Zap,
RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
import { keyframes } from '@emotion/react'; import { keyframes } from '@emotion/react';
import ConceptTimelineModal from './ConceptTimelineModal'; import ConceptTimelineModal from './ConceptTimelineModal';
@@ -123,6 +124,7 @@ import ConceptStatsPanel from './components/ConceptStatsPanel';
import HierarchyView from './components/HierarchyView'; import HierarchyView from './components/HierarchyView';
import ForceGraphView from './components/ForceGraphView'; import ForceGraphView from './components/ForceGraphView';
import BreadcrumbNav from './components/BreadcrumbNav'; import BreadcrumbNav from './components/BreadcrumbNav';
import ChartContainer from './components/ChartContainer';
import ConceptStocksModal from '@components/ConceptStocksModal'; import ConceptStocksModal from '@components/ConceptStocksModal';
import TradeDatePicker from '@components/TradeDatePicker'; import TradeDatePicker from '@components/TradeDatePicker';
// 导航栏已由 MainLayout 提供,无需在此导入 // 导航栏已由 MainLayout 提供,无需在此导入
@@ -367,6 +369,9 @@ const ConceptCenter = () => {
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [viewMode, setViewMode] = useState('list'); // 默认列表视图 const [viewMode, setViewMode] = useState('list'); // 默认列表视图
// 图表共享的钻取路径状态tab 切换时保持)
const [chartDrillPath, setChartDrillPath] = useState(null);
// 层级筛选状态 // 层级筛选状态
const [hierarchyFilter, setHierarchyFilter] = useState({ lv1: null, lv2: null, lv3: null }); const [hierarchyFilter, setHierarchyFilter] = useState({ lv1: null, lv2: null, lv3: null });
@@ -590,6 +595,41 @@ const ConceptCenter = () => {
fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter); fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter);
}, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]); }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]);
// 图表面包屑导航项(从 chartDrillPath 派生)
const chartBreadcrumbItems = useMemo(() => {
const items = [{ label: '全部分类', path: null }];
if (chartDrillPath?.lv1) {
items.push({ label: chartDrillPath.lv1, path: { lv1: chartDrillPath.lv1 } });
}
if (chartDrillPath?.lv2) {
items.push({ label: chartDrillPath.lv2, path: { lv1: chartDrillPath.lv1, lv2: chartDrillPath.lv2 } });
}
if (chartDrillPath?.lv3) {
items.push({ label: chartDrillPath.lv3, path: { lv1: chartDrillPath.lv1, lv2: chartDrillPath.lv2, lv3: chartDrillPath.lv3 } });
}
return items;
}, [chartDrillPath]);
// 图表面包屑导航回调
const handleChartNavigate = useCallback((path) => {
setChartDrillPath(path);
}, []);
// 图表返回上一层
const handleChartGoBack = useCallback(() => {
if (!chartDrillPath) return;
if (chartDrillPath.lv3) {
setChartDrillPath({ lv1: chartDrillPath.lv1, lv2: chartDrillPath.lv2 });
} else if (chartDrillPath.lv2) {
setChartDrillPath({ lv1: chartDrillPath.lv1 });
} else if (chartDrillPath.lv1) {
setChartDrillPath(null);
}
}, [chartDrillPath]);
// 处理搜索 // 处理搜索
const handleSearch = () => { const handleSearch = () => {
setCurrentPage(1); setCurrentPage(1);
@@ -1437,8 +1477,8 @@ const ConceptCenter = () => {
</Card> </Card>
); );
// 日期选择组件 - 深色主题 // 统一控制栏组件 - 日期 + 排序 + 视图切换(单行布局)
const DateSelector = () => ( const ControlBar = () => (
<Box <Box
p={4} p={4}
bg="rgba(15, 23, 42, 0.8)" bg="rgba(15, 23, 42, 0.8)"
@@ -1448,119 +1488,187 @@ const ConceptCenter = () => {
borderColor="whiteAlpha.100" borderColor="whiteAlpha.100"
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)" boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
> >
<Flex <Flex justify="space-between" align="center" wrap="wrap" gap={4}>
direction={{ base: 'column', lg: 'row' }} {/* 左侧:日期选择区 */}
align={{ base: 'stretch', lg: 'center' }} <HStack spacing={3} flexWrap="wrap">
gap={4} <ButtonGroup size="sm" spacing={1}>
> {[
{/* 使用通用日期选择器组件 - 不显示最新日期提示,由下方单独渲染 */} { label: '今天', days: 0 },
<TradeDatePicker { label: '昨天', days: 1 },
value={selectedDate} { label: '一周前', days: 7 },
onChange={(date) => { { label: '一月前', days: 30 },
const dateStr = date.toISOString().split('T')[0]; ].map(({ label, days }) => {
const previousDate = selectedDate ? selectedDate.toISOString().split('T')[0] : null; // 判断是否选中当前按钮
trackFilterApplied('date', dateStr, previousDate); const targetDate = new Date();
setSelectedDate(date); targetDate.setDate(targetDate.getDate() - days);
setCurrentPage(1); const isSelected = selectedDate &&
updateUrlParams({ date: dateStr, page: 1 }); selectedDate.toDateString() === targetDate.toDateString();
fetchConcepts(searchQuery, 1, date, sortBy);
}}
latestTradeDate={latestTradeDate}
label="交易日期"
showLatestTradeDateTip={false}
/>
{/* 快捷按钮 - 紧跟日期选择器 */} return (
<ButtonGroup size="sm" flexWrap="wrap" spacing={2}> <Button
<Button key={days}
onClick={() => handleQuickDateSelect(0)} onClick={() => handleQuickDateSelect(days)}
bg="whiteAlpha.100" bg={isSelected ? 'purple.500' : 'whiteAlpha.100'}
color="white" color="white"
borderRadius="full" borderRadius="full"
border="1px solid" border="1px solid"
borderColor="whiteAlpha.200" borderColor={isSelected ? 'purple.500' : 'whiteAlpha.200'}
px={4} px={3}
_hover={{ size="sm"
bg: 'purple.500', boxShadow={isSelected ? '0 0 12px rgba(139, 92, 246, 0.4)' : 'none'}
borderColor: 'purple.500', _hover={{
boxShadow: '0 0 12px rgba(139, 92, 246, 0.4)', bg: 'purple.500',
borderColor: 'purple.500',
boxShadow: '0 0 12px rgba(139, 92, 246, 0.4)',
}}
transition="all 0.2s"
>
{label}
</Button>
);
})}
</ButtonGroup>
<TradeDatePicker
value={selectedDate}
onChange={(date) => {
const dateStr = date.toISOString().split('T')[0];
const previousDate = selectedDate ? selectedDate.toISOString().split('T')[0] : null;
trackFilterApplied('date', dateStr, previousDate);
setSelectedDate(date);
setCurrentPage(1);
updateUrlParams({ date: dateStr, page: 1 });
fetchConcepts(searchQuery, 1, date, sortBy);
}} }}
transition="all 0.2s" latestTradeDate={latestTradeDate}
> label="更多"
今天 showIcon={false}
</Button> showLatestTradeDateTip={false}
<Button inputWidth="130px"
onClick={() => handleQuickDateSelect(1)} size="sm"
bg="whiteAlpha.100" labelColor="white"
color="white" />
borderRadius="full" {/* 数据更新提示 */}
border="1px solid" {latestTradeDate && (
borderColor="whiteAlpha.200" <Text fontSize="xs" color="red.400" whiteSpace="nowrap">
px={4} 更新至 {latestTradeDate.toLocaleDateString('zh-CN')}
_hover={{ </Text>
bg: 'purple.500', )}
borderColor: 'purple.500', </HStack>
boxShadow: '0 0 12px rgba(139, 92, 246, 0.4)',
}}
transition="all 0.2s"
>
昨天
</Button>
<Button
onClick={() => handleQuickDateSelect(7)}
bg="whiteAlpha.100"
color="white"
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
px={4}
_hover={{
bg: 'purple.500',
borderColor: 'purple.500',
boxShadow: '0 0 12px rgba(139, 92, 246, 0.4)',
}}
transition="all 0.2s"
>
一周前
</Button>
<Button
onClick={() => handleQuickDateSelect(30)}
bg="whiteAlpha.100"
color="white"
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
px={4}
_hover={{
bg: 'purple.500',
borderColor: 'purple.500',
boxShadow: '0 0 12px rgba(139, 92, 246, 0.4)',
}}
transition="all 0.2s"
>
一月前
</Button>
</ButtonGroup>
{/* 最新交易日期提示 - 靠右显示 */} {/* 右侧:刷新 + 排序 + 视图切换 */}
{latestTradeDate && ( <HStack spacing={4} align="center" flexWrap="wrap">
<Tooltip label="数据库中最新的交易日期"> {/* 刷新按钮 */}
<HStack <Tooltip label="刷新数据" placement="top">
spacing={1.5} <IconButton
ml="auto" size="sm"
px={2} icon={<RefreshCw size={16} />}
py={1} onClick={() => fetchConcepts(searchQuery, currentPage, selectedDate, sortBy)}
opacity={0.7} bg="whiteAlpha.100"
_hover={{ opacity: 1 }} color="white"
transition="opacity 0.2s" border="1px solid"
> borderColor="whiteAlpha.200"
<Icon as={Info} color="blue.300" boxSize={3} /> borderRadius="full"
<Text fontSize="xs" color="blue.200"> _hover={{
数据更新至 {latestTradeDate.toLocaleDateString('zh-CN')} bg: 'whiteAlpha.200',
</Text> borderColor: 'whiteAlpha.300',
</HStack> }}
transition="all 0.2s"
aria-label="刷新"
/>
</Tooltip> </Tooltip>
)}
{/* 排序下拉框 - 仅列表视图显示 */}
{viewMode === 'list' && (
<HStack spacing={2}>
<Text fontSize="sm" color="whiteAlpha.600" whiteSpace="nowrap">排序</Text>
<Select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value)}
width="120px"
size="sm"
focusBorderColor="purple.400"
borderColor="whiteAlpha.300"
borderRadius="lg"
fontWeight="medium"
color="white"
bg="whiteAlpha.50"
_hover={{ borderColor: 'purple.400' }}
sx={{
option: {
bg: 'gray.800',
color: 'white',
},
}}
>
<option value="change_pct">涨跌幅</option>
<option value="_score">相关度</option>
<option value="stock_count">股票数量</option>
<option value="outbreak_date">爆发日期</option>
</Select>
</HStack>
)}
{/* 视图切换按钮组 */}
<ButtonGroup size="sm" isAttached variant="outline">
<Tooltip label="矩形树图" placement="top">
<IconButton
icon={<BoxIcon size={16} />}
onClick={() => {
if (viewMode !== 'force3d') {
trackViewModeChanged('force3d', viewMode);
setViewMode('force3d');
}
}}
bg={viewMode === 'force3d' ? 'purple.500' : 'transparent'}
color={viewMode === 'force3d' ? 'white' : 'whiteAlpha.700'}
borderColor="whiteAlpha.300"
_hover={{
bg: viewMode === 'force3d' ? 'purple.400' : 'whiteAlpha.100',
boxShadow: viewMode === 'force3d' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
}}
aria-label="矩形树图"
/>
</Tooltip>
<Tooltip label="层级图" placement="top">
<IconButton
icon={<Network />}
onClick={() => {
if (viewMode !== 'hierarchy') {
trackViewModeChanged('hierarchy', viewMode);
setViewMode('hierarchy');
}
}}
bg={viewMode === 'hierarchy' ? 'purple.500' : 'transparent'}
color={viewMode === 'hierarchy' ? 'white' : 'whiteAlpha.700'}
borderColor="whiteAlpha.300"
_hover={{
bg: viewMode === 'hierarchy' ? 'purple.400' : 'whiteAlpha.100',
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
}}
aria-label="层级图"
/>
</Tooltip>
<Tooltip label="列表视图" placement="top">
<IconButton
icon={<List />}
onClick={() => {
if (viewMode !== 'list') {
trackViewModeChanged('list', viewMode);
setViewMode('list');
}
}}
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
color={viewMode === 'list' ? 'white' : 'whiteAlpha.700'}
borderColor="whiteAlpha.300"
_hover={{
bg: viewMode === 'list' ? 'purple.400' : 'whiteAlpha.100',
boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
}}
aria-label="列表视图"
/>
</Tooltip>
</ButtonGroup>
</HStack>
</Flex> </Flex>
</Box> </Box>
); );
@@ -1626,179 +1734,57 @@ const ConceptCenter = () => {
} }
/> />
{/* 主内容区域 - padding 由 MainLayout 统一设置 */} {/* 主内容区域 - 左右两栏布局 */}
<Box py={10} position="relative" zIndex={1}> <Box pt={0} pb={10} position="relative" zIndex={1}>
<Box mb={6}> <Flex gap={6} direction={{ base: 'column', xl: 'row' }} align="flex-start">
<DateSelector /> {/* 左侧:控制栏 + 概念内容 */}
</Box> <Box flex="1" minW={0}>
{/* 控制栏 */}
<Box mb={6}>
<ControlBar />
</Box>
{/* 双栏布局:左侧概念卡片,右侧统计面板 */} {/* 概念内容区域 */}
<Flex gap={8} direction={{ base: 'column', xl: 'row' }}> <Box>
{/* 左侧概念卡片区域 */} {/* 面包屑导航 - 显示当前层级筛选 */}
<Box flex={1}> <BreadcrumbNav
filter={hierarchyFilter}
onClearFilter={handleClearHierarchyFilter}
onNavigate={handleNavigateHierarchy}
isDarkMode={true}
/>
<Card {/* 矩形树图 / 层级图视图 - 使用统一容器 */}
mb={8} {(viewMode === 'force3d' || viewMode === 'hierarchy') ? (
bg="rgba(15, 23, 42, 0.8)" <ChartContainer
backdropFilter={GLASS_BLUR.lg} breadcrumbItems={chartBreadcrumbItems}
border="1px solid" onNavigate={handleChartNavigate}
borderColor="whiteAlpha.100" onGoBack={handleChartGoBack}
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)" showBackButton={!!chartDrillPath}
borderRadius="2xl" showLegend={viewMode === 'hierarchy'}
contentTopPadding={viewMode === 'force3d' ? 0 : 14}
> >
<CardBody> {viewMode === 'force3d' ? (
<Flex <ForceGraphView
direction={{ base: 'column', md: 'row' }} apiBaseUrl={API_BASE_URL}
justify="space-between" onSelectCategory={handleHierarchySelect}
align={{ base: 'stretch', md: 'center' }} selectedDate={selectedDate}
gap={4} externalDrillPath={chartDrillPath}
> onDrillPathChange={setChartDrillPath}
{/* 排序方式 - 仅在列表视图显示 */} hideNavigation={true}
{viewMode === 'list' && ( />
<HStack spacing={4} align="center"> ) : (
<Icon as={Tags} boxSize={4} color="purple.300" /> <HierarchyView
<Text fontWeight="bold" color="white">排序方式</Text> apiBaseUrl={API_BASE_URL}
<Select onSelectCategory={handleHierarchySelect}
value={sortBy} selectedDate={selectedDate}
onChange={(e) => handleSortChange(e.target.value)} externalDrillPath={chartDrillPath}
width="200px" onDrillPathChange={setChartDrillPath}
focusBorderColor="purple.400" hideNavigation={true}
borderColor="whiteAlpha.300" />
borderRadius="lg" )}
fontWeight="medium" </ChartContainer>
color="white" ) : loading ? (
bg="whiteAlpha.50"
_hover={{ borderColor: 'purple.400' }}
sx={{
option: {
bg: 'gray.800',
color: 'white',
},
}}
>
<option value="change_pct">涨跌幅</option>
<option value="_score">相关度</option>
<option value="stock_count">股票数量</option>
<option value="outbreak_date">爆发日期</option>
</Select>
{searchQuery && sortBy === '_score' && (
<Tooltip label="搜索时自动切换到相关度排序,以显示最匹配的结果。您也可以手动切换其他排序方式。">
<HStack
spacing={1}
bg="blue.500"
px={3}
py={1}
borderRadius="full"
boxShadow="0 0 10px rgba(59, 130, 246, 0.4)"
>
<Icon as={Info} color="white" boxSize={3} />
<Text fontSize="xs" color="white" fontWeight="medium">
智能排序
</Text>
</HStack>
</Tooltip>
)}
</HStack>
)}
<ButtonGroup size="sm" isAttached variant="outline" ml={viewMode !== 'list' ? 'auto' : undefined}>
<Tooltip label="概念矩形树图" placement="top">
<IconButton
icon={<BoxIcon size={16} />}
onClick={() => {
if (viewMode !== 'force3d') {
trackViewModeChanged('force3d', viewMode);
setViewMode('force3d');
}
}}
bg={viewMode === 'force3d' ? 'purple.500' : 'transparent'}
color={viewMode === 'force3d' ? 'white' : 'whiteAlpha.700'}
borderColor="whiteAlpha.300"
_hover={{
bg: viewMode === 'force3d' ? 'purple.400' : 'whiteAlpha.100',
boxShadow: viewMode === 'force3d' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
}}
aria-label="概念矩形树图"
/>
</Tooltip>
<Tooltip label="层级图" placement="top">
<IconButton
icon={<Network />}
onClick={() => {
if (viewMode !== 'hierarchy') {
trackViewModeChanged('hierarchy', viewMode);
setViewMode('hierarchy');
}
}}
bg={viewMode === 'hierarchy' ? 'purple.500' : 'transparent'}
color={viewMode === 'hierarchy' ? 'white' : 'whiteAlpha.700'}
borderColor="whiteAlpha.300"
_hover={{
bg: viewMode === 'hierarchy' ? 'purple.400' : 'whiteAlpha.100',
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
}}
aria-label="层级图"
/>
</Tooltip>
<Tooltip label="列表视图" placement="top">
<IconButton
icon={<List />}
onClick={() => {
if (viewMode !== 'list') {
trackViewModeChanged('list', viewMode);
setViewMode('list');
}
}}
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
color={viewMode === 'list' ? 'white' : 'whiteAlpha.700'}
borderColor="whiteAlpha.300"
_hover={{
bg: viewMode === 'list' ? 'purple.400' : 'whiteAlpha.100',
boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
}}
aria-label="列表视图"
/>
</Tooltip>
</ButtonGroup>
</Flex>
</CardBody>
</Card>
{/* 面包屑导航 - 显示当前层级筛选 */}
<BreadcrumbNav
filter={hierarchyFilter}
onClearFilter={handleClearHierarchyFilter}
onNavigate={handleNavigateHierarchy}
isDarkMode={true}
/>
{selectedDate && viewMode !== 'hierarchy' && viewMode !== 'force3d' && (
<Box mb={4} p={3} bg="rgba(59, 130, 246, 0.2)" borderRadius="xl" borderLeft="4px solid" borderColor="blue.400">
<HStack>
<Icon as={Info} color="blue.300" />
<Text fontSize="sm" color="whiteAlpha.800">
当前显示 <Text as="strong" color="cyan.300">{selectedDate.toLocaleDateString('zh-CN')}</Text>
{searchQuery && <span>搜索词<Text as="strong" color="cyan.300">"{searchQuery}"</Text></span>}
</Text>
</HStack>
</Box>
)}
{/* 3D 力导向图视图 */}
{viewMode === 'force3d' ? (
<ForceGraphView
apiBaseUrl={API_BASE_URL}
onSelectCategory={handleHierarchySelect}
selectedDate={selectedDate}
/>
) : /* 层级图视图 */
viewMode === 'hierarchy' ? (
<HierarchyView
apiBaseUrl={API_BASE_URL}
onSelectCategory={handleHierarchySelect}
selectedDate={selectedDate}
/>
) : loading ? (
<SimpleGrid columns={{ base: 2, md: 2, lg: 3 }} spacing={{ base: 3, md: 6 }}> <SimpleGrid columns={{ base: 2, md: 2, lg: 3 }} spacing={{ base: 3, md: 6 }}>
{[...Array(12)].map((_, i) => ( {[...Array(12)].map((_, i) => (
<SkeletonCard key={i} /> <SkeletonCard key={i} />
@@ -1933,53 +1919,35 @@ const ConceptCenter = () => {
</VStack> </VStack>
</Center> </Center>
) : null} ) : null}
</Box>
</Box> </Box>
{/* 右侧统计面板 */} {/* 右侧:概念统计中心 */}
<Box w={{ base: '100%', xl: '400px' }} flexShrink={0}> <Box w={{ base: '100%', xl: '400px' }} flexShrink={0}>
<Box position="sticky" top={6}> {hasFeatureAccess('concept_stats_panel') ? (
{hasFeatureAccess('concept_stats_panel') ? ( <ConceptStatsPanel
<ConceptStatsPanel apiBaseUrl={API_BASE_URL}
apiBaseUrl={API_BASE_URL} onConceptClick={handleConceptClick}
onConceptClick={handleConceptClick} isDarkMode={true}
isDarkMode={true} />
/> ) : (
) : ( <Card
<Card bg="rgba(15, 23, 42, 0.8)"
bg="rgba(15, 23, 42, 0.8)" backdropFilter={GLASS_BLUR.lg}
backdropFilter={GLASS_BLUR.lg} border="1px solid"
border="1px solid" borderColor="whiteAlpha.100"
borderColor="whiteAlpha.100" borderRadius="2xl"
borderRadius="2xl" >
> <CardBody p={4}>
<CardBody p={6}> <HStack spacing={3}>
<VStack spacing={4} textAlign="center"> <Icon as={LineChart} boxSize={6} color="whiteAlpha.300" />
<Icon as={LineChart} boxSize={12} color="whiteAlpha.300" /> <Text fontSize="sm" color="whiteAlpha.600">
<VStack spacing={2}> 概念统计中心需要Pro版订阅
<Heading size="md" color="white"> </Text>
概念统计中心 </HStack>
</Heading> </CardBody>
<Text fontSize="sm" color="whiteAlpha.600"> </Card>
此功能需要Pro版订阅才能使用 )}
</Text>
</VStack>
<Button
bg="blue.500"
color="white"
leftIcon={<Icon as={Rocket} />}
onClick={() => {
setUpgradeFeature('pro');
setUpgradeModalOpen(true);
}}
_hover={{ bg: 'blue.400', boxShadow: '0 0 15px rgba(59, 130, 246, 0.5)' }}
>
升级到Pro版
</Button>
</VStack>
</CardBody>
</Card>
)}
</Box>
</Box> </Box>
</Flex> </Flex>
</Box> </Box>