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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user