This commit is contained in:
2025-10-11 12:10:00 +08:00
parent 8107dee8d3
commit 4d0dc109bc
109 changed files with 152150 additions and 8037 deletions

View File

@@ -78,18 +78,12 @@ import {
Collapse,
} from '@chakra-ui/react';
import { SearchIcon, ViewIcon, CalendarIcon, ExternalLinkIcon, StarIcon, ChevronDownIcon, InfoIcon, CloseIcon, ChevronRightIcon } from '@chakra-ui/icons';
import { FaThLarge, FaList, FaTags, FaChartLine, FaRobot, FaTable, FaHistory, FaBrain, FaLightbulb, FaRocket, FaShieldAlt, FaCalendarAlt, FaArrowUp, FaArrowDown, FaNewspaper, FaFileAlt, FaExpand, FaCompress, FaClock, FaLock } from 'react-icons/fa';
import { FaThLarge, FaList, FaTags, FaChartLine, FaRobot, FaTable, FaHistory, FaBrain, FaLightbulb, FaRocket, FaShieldAlt, FaCalendarAlt, FaArrowUp, FaArrowDown, FaNewspaper, FaFileAlt, FaExpand, FaCompress } from 'react-icons/fa';
import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
import { keyframes } from '@emotion/react';
import ConceptTimelineModal from './ConceptTimelineModal';
import ConceptStatsPanel from './components/ConceptStatsPanel';
// 导入导航栏组件
import HomeNavbar from '../../components/Navbars/HomeNavbar';
// 导入订阅权限管理
import { useSubscription } from '../../hooks/useSubscription';
import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal';
// 导入市场服务
import { marketService } from '../../services/marketService';
const API_BASE_URL = process.env.NODE_ENV === 'production'
? '/concept-api'
@@ -129,11 +123,6 @@ const ConceptCenter = () => {
const navigate = useNavigate();
const toast = useToast();
// 订阅权限管理
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
const [upgradeFeature, setUpgradeFeature] = useState('pro');
// 状态管理
const [concepts, setConcepts] = useState([]);
const [loading, setLoading] = useState(false);
@@ -156,9 +145,6 @@ const ConceptCenter = () => {
const [selectedConceptName, setSelectedConceptName] = useState('');
const [isTimelineModalOpen, setIsTimelineModalOpen] = useState(false);
const [selectedConceptId, setSelectedConceptId] = useState('');
// 股票行情数据状态
const [stockMarketData, setStockMarketData] = useState({});
const [loadingStockData, setLoadingStockData] = useState(false);
// 默认图片路径
const defaultImage = '/assets/img/default-event.jpg';
@@ -180,18 +166,9 @@ const ConceptCenter = () => {
}
return null;
}, []);
// 打开内容模态框(新闻和研报)- 需要Max版权限
// 打开内容模态框(新闻和研报)
const handleViewContent = (e, conceptName, conceptId) => {
e.stopPropagation();
// 检查历史时间轴权限
if (!hasFeatureAccess('concept_timeline')) {
const recommendation = getUpgradeRecommendation('concept_timeline');
setUpgradeFeature(recommendation?.required || 'max');
setUpgradeModalOpen(true);
return;
}
setSelectedConceptForContent(conceptName);
setSelectedConceptId(conceptId);
setIsTimelineModalOpen(true);
@@ -370,78 +347,16 @@ const ConceptCenter = () => {
// 处理概念点击
const handleConceptClick = (conceptId, conceptName) => {
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`;
const htmlPath = `/htmls/${conceptName}.html`;
window.open(htmlPath, '_blank');
};
// 获取股票行情数据
const fetchStockMarketData = async (stocks) => {
if (!stocks || stocks.length === 0) return;
setLoadingStockData(true);
const newMarketData = {};
try {
// 批量获取股票数据每次处理5个股票以避免并发过多
const batchSize = 5;
for (let i = 0; i < stocks.length; i += batchSize) {
const batch = stocks.slice(i, i + batchSize);
const promises = batch.map(async (stock) => {
if (!stock.stock_code) return null;
// 提取6位股票代码去掉交易所后缀
const seccode = stock.stock_code.substring(0, 6);
try {
const response = await marketService.getTradeData(seccode, 1);
if (response.success && response.data && response.data.length > 0) {
const latestData = response.data[response.data.length - 1];
return {
stock_code: stock.stock_code,
...latestData
};
}
} catch (error) {
console.warn(`获取股票 ${seccode} 行情数据失败:`, error);
}
return null;
});
const batchResults = await Promise.all(promises);
batchResults.forEach(result => {
if (result) {
newMarketData[result.stock_code] = result;
}
});
}
setStockMarketData(newMarketData);
} catch (error) {
console.error('批量获取股票行情数据失败:', error);
} finally {
setLoadingStockData(false);
}
};
// 打开股票详情Modal - 需要Pro版权限
// 打开股票详情Modal
const handleViewStocks = (e, concept) => {
e.stopPropagation();
// 检查热门个股权限
if (!hasFeatureAccess('hot_stocks')) {
const recommendation = getUpgradeRecommendation('hot_stocks');
setUpgradeFeature(recommendation?.required || 'pro');
setUpgradeModalOpen(true);
return;
}
setSelectedConceptStocks(concept.stocks || []);
setSelectedConceptName(concept.concept);
setStockMarketData({}); // 清空之前的数据
setIsStockModalOpen(true);
// 获取股票行情数据
fetchStockMarketData(concept.stocks || []);
};
// 格式化涨跌幅显示
@@ -457,33 +372,6 @@ const ConceptCenter = () => {
return value > 0 ? 'red' : value < 0 ? 'green' : 'gray';
};
// 格式化价格显示
const formatPrice = (value) => {
if (value === null || value === undefined) return '-';
return `¥${value.toFixed(2)}`;
};
// 格式化涨跌幅显示(股票表格专用)
const formatStockChangePercent = (value) => {
if (value === null || value === undefined) return '-';
const formatted = value.toFixed(2);
return value >= 0 ? `+${formatted}%` : `${formatted}%`;
};
// 获取涨跌幅颜色(股票表格专用)
const getStockChangeColor = (value) => {
if (value === null || value === undefined) return 'gray';
return value > 0 ? 'red' : value < 0 ? 'green' : 'gray';
};
// 生成公司详情链接
const generateCompanyLink = (stockCode) => {
if (!stockCode) return '#';
// 提取6位股票代码
const seccode = stockCode.substring(0, 6);
return `https://valuefrontier.cn/company?scode=${seccode}`;
};
// 渲染动态表格列
const renderStockTable = () => {
if (!selectedConceptStocks || selectedConceptStocks.length === 0) {
@@ -495,8 +383,7 @@ const ConceptCenter = () => {
Object.keys(stock).forEach(key => allFields.add(key));
});
// 定义固定的列顺序,包含新增的现价和涨跌幅列
const orderedFields = ['stock_name', 'stock_code', 'current_price', 'change_percent'];
const orderedFields = ['stock_name', 'stock_code'];
allFields.forEach(field => {
if (!orderedFields.includes(field)) {
orderedFields.push(field);
@@ -504,86 +391,31 @@ const ConceptCenter = () => {
});
return (
<Box>
{loadingStockData && (
<Box mb={4} textAlign="center">
<HStack justify="center" spacing={2}>
<Spinner size="sm" color="purple.500" />
<Text fontSize="sm" color="gray.600">正在获取行情数据...</Text>
</HStack>
</Box>
)}
<TableContainer maxH="60vh" overflowY="auto">
<Table variant="simple" size="sm">
<Thead position="sticky" top={0} bg="white" zIndex={1}>
<Tr>
<TableContainer maxH="60vh" overflowY="auto">
<Table variant="simple" size="sm">
<Thead position="sticky" top={0} bg="white" zIndex={1}>
<Tr>
{orderedFields.map(field => (
<Th key={field}>
{field === 'stock_name' ? '股票名称' :
field === 'stock_code' ? '股票代码' : field}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{selectedConceptStocks.map((stock, idx) => (
<Tr key={idx} _hover={{ bg: 'gray.50' }}>
{orderedFields.map(field => (
<Th key={field}>
{field === 'stock_name' ? '股票名称' :
field === 'stock_code' ? '股票代码' :
field === 'current_price' ? '现价' :
field === 'change_percent' ? '当日涨跌幅' : field}
</Th>
<Td key={field}>
{stock[field] || '-'}
</Td>
))}
</Tr>
</Thead>
<Tbody>
{selectedConceptStocks.map((stock, idx) => {
const marketData = stockMarketData[stock.stock_code];
const companyLink = generateCompanyLink(stock.stock_code);
return (
<Tr key={idx} _hover={{ bg: 'gray.50' }}>
{orderedFields.map(field => {
let cellContent = stock[field] || '-';
let cellProps = {};
// 处理特殊字段
if (field === 'current_price') {
cellContent = marketData ? formatPrice(marketData.close) : (loadingStockData ? <Spinner size="xs" /> : '-');
} else if (field === 'change_percent') {
if (marketData) {
cellContent = formatStockChangePercent(marketData.change_percent);
cellProps.color = `${getStockChangeColor(marketData.change_percent)}.500`;
cellProps.fontWeight = 'bold';
} else {
cellContent = loadingStockData ? <Spinner size="xs" /> : '-';
}
} else if (field === 'stock_name' || field === 'stock_code') {
// 添加超链接
cellContent = (
<Text
as="a"
href={companyLink}
target="_blank"
rel="noopener noreferrer"
color="blue.600"
textDecoration="underline"
_hover={{
color: 'blue.800',
textDecoration: 'underline'
}}
cursor="pointer"
>
{stock[field] || '-'}
</Text>
);
}
return (
<Td key={field} {...cellProps}>
{cellContent}
</Td>
);
})}
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</Box>
))}
</Tbody>
</Table>
</TableContainer>
);
};
@@ -776,45 +608,23 @@ const ConceptCenter = () => {
>
<Flex align="center" justify="space-between">
<Box flex={1}>
<HStack spacing={2} mb={1}>
<Text fontSize="xs" color="gray.600" fontWeight="medium">
热门个股
</Text>
{!hasFeatureAccess('hot_stocks') && (
<Badge colorScheme="yellow" size="sm">
🔒需Pro
</Badge>
)}
</HStack>
<Text fontSize="xs" color="gray.600" fontWeight="medium" mb={1}>
热门个股
</Text>
<HStack spacing={1} flexWrap="wrap">
{hasFeatureAccess('hot_stocks') ? (
<>
{concept.stocks.slice(0, 2).map((stock, idx) => (
<Tag key={idx} size="sm" colorScheme="purple" variant="subtle">
<TagLabel fontSize="xs">{stock.stock_name}</TagLabel>
</Tag>
))}
{concept.stocks.length > 2 && (
<Text fontSize="xs" color="purple.600" fontWeight="medium">
+{concept.stocks.length - 2}更多
</Text>
)}
</>
) : (
<HStack spacing={1}>
<Icon as={FaLock} boxSize="10px" color="yellow.600" />
<Text fontSize="xs" color="yellow.600" fontWeight="medium">
升级查看{concept.stocks.length}只个股
</Text>
</HStack>
{concept.stocks.slice(0, 2).map((stock, idx) => (
<Tag key={idx} size="sm" colorScheme="purple" variant="subtle">
<TagLabel fontSize="xs">{stock.stock_name}</TagLabel>
</Tag>
))}
{concept.stocks.length > 2 && (
<Text fontSize="xs" color="purple.600" fontWeight="medium">
+{concept.stocks.length - 2}更多
</Text>
)}
</HStack>
</Box>
<Icon
as={hasFeatureAccess('hot_stocks') ? ChevronRightIcon : FaLock}
color={hasFeatureAccess('hot_stocks') ? 'purple.500' : 'yellow.600'}
boxSize={4}
/>
<Icon as={ChevronRightIcon} color="purple.500" boxSize={4} />
</Flex>
</Box>
)}
@@ -858,173 +668,6 @@ const ConceptCenter = () => {
);
};
// 概念列表项组件 - 列表视图
const ConceptListItem = ({ concept }) => {
const changePercent = concept.price_info?.avg_change_pct;
const changeColor = getChangeColor(changePercent);
const hasChange = changePercent !== null && changePercent !== undefined;
return (
<Card
cursor="pointer"
onClick={() => handleConceptClick(concept.concept_id, concept.concept)}
bg="white"
borderWidth="1px"
borderColor="gray.200"
overflow="hidden"
_hover={{
transform: 'translateX(4px)',
boxShadow: 'lg',
borderColor: 'purple.300',
}}
transition="all 0.3s"
>
<CardBody p={6}>
<Flex align="center" gap={6}>
{/* 左侧图标区域 */}
<Box
width="80px"
height="80px"
borderRadius="xl"
bgGradient="linear(to-br, purple.100, pink.100)"
display="flex"
alignItems="center"
justifyContent="center"
position="relative"
flexShrink={0}
>
<Icon as={FaTags} boxSize={8} color="purple.400" />
{hasChange && (
<Badge
position="absolute"
top={-2}
right={-2}
bg={changeColor === 'red' ? 'red.500' : changeColor === 'green' ? 'green.500' : 'gray.500'}
color="white"
fontSize="xs"
px={2}
py={1}
borderRadius="full"
fontWeight="bold"
minW="auto"
>
<Icon
as={changePercent > 0 ? FaArrowUp : changePercent < 0 ? FaArrowDown : null}
boxSize={2}
mr={1}
/>
{formatChangePercent(changePercent)}
</Badge>
)}
</Box>
{/* 中间内容区域 */}
<Box flex={1}>
<VStack align="start" spacing={3}>
<Heading size="md" color="gray.800" noOfLines={1}>
{concept.concept}
</Heading>
<Text color="gray.600" fontSize="sm" noOfLines={2}>
{concept.description || '该概念板块涵盖相关技术、产业链和市场应用等多个维度的投资机会'}
</Text>
<HStack spacing={4} flexWrap="wrap">
<HStack spacing={1}>
<Icon as={FaChartLine} boxSize={4} color="purple.500" />
<Text fontSize="sm" fontWeight="medium" color="gray.700">
{concept.stock_count || 0} 只股票
</Text>
</HStack>
{hasChange && concept.price_info?.trade_date && (
<HStack spacing={1}>
<Icon as={FaCalendarAlt} boxSize={4} color="blue.500" />
<Text fontSize="sm" color="gray.600">
{new Date(concept.price_info.trade_date).toLocaleDateString('zh-CN')}
</Text>
</HStack>
)}
{formatHappenedTimes(concept.happened_times)}
</HStack>
</VStack>
</Box>
{/* 右侧操作区域 */}
<VStack spacing={3} align="end" flexShrink={0}>
<HStack spacing={3}>
<Button
size="sm"
leftIcon={<ViewIcon />}
colorScheme="blue"
variant="outline"
onClick={(e) => handleViewStocks(e, concept)}
borderRadius="full"
>
查看个股
</Button>
<Button
size="sm"
leftIcon={<FaChartLine />}
colorScheme="purple"
variant="solid"
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id)}
borderRadius="full"
>
历史时间轴
</Button>
</HStack>
{concept.stocks && concept.stocks.length > 0 && (
<Box>
<HStack spacing={1} mb={2}>
<Text fontSize="xs" color="gray.500">热门个股</Text>
{!hasFeatureAccess('hot_stocks') && (
<Badge colorScheme="yellow" size="sm">
🔒需Pro
</Badge>
)}
</HStack>
<Wrap spacing={1} justify="end">
{hasFeatureAccess('hot_stocks') ? (
<>
{concept.stocks.slice(0, 3).map((stock, idx) => (
<WrapItem key={idx}>
<Tag size="sm" colorScheme="purple" variant="subtle">
<TagLabel fontSize="xs">{stock.stock_name}</TagLabel>
</Tag>
</WrapItem>
))}
{concept.stocks.length > 3 && (
<WrapItem>
<Text fontSize="xs" color="purple.600" fontWeight="medium">
+{concept.stocks.length - 3}更多
</Text>
</WrapItem>
)}
</>
) : (
<WrapItem>
<HStack spacing={1}>
<Icon as={FaLock} boxSize="8px" color="yellow.600" />
<Text fontSize="xs" color="yellow.600" fontWeight="medium">
升级查看{concept.stocks.length}
</Text>
</HStack>
</WrapItem>
)}
</Wrap>
</Box>
)}
</VStack>
</Flex>
</CardBody>
</Card>
);
};
// 骨架屏组件
const SkeletonCard = () => (
<Card bg="white" borderWidth="1px" borderColor="gray.200">
@@ -1125,38 +768,31 @@ const ConceptCenter = () => {
opacity={0.5}
/>
<Container maxW="container.xl" position="relative" py={{ base: 12, md: 20 }}>
<VStack spacing={8}>
<VStack spacing={4} textAlign="center">
<Container maxW="container.xl" position="relative" py={{ base: 20, md: 32 }}>
<VStack spacing={12}>
<VStack spacing={6} textAlign="center">
<HStack spacing={4} justify="center">
<Icon as={FaBrain} boxSize={16} color="yellow.300" />
</HStack>
<VStack spacing={2}>
<VStack spacing={3}>
<Heading
as="h1"
size="2xl"
size="3xl"
fontWeight="black"
bgGradient="linear(to-r, white, yellow.200)"
bgClip="text"
letterSpacing="tight"
lineHeight="shorter"
>
概念中心
</Heading>
<HStack spacing={2} justify="center">
<Icon as={FaClock} boxSize={4} color="yellow.200" />
<Text fontSize="sm" fontWeight="medium" opacity={0.95}>
约下午4点更新
</Text>
</HStack>
<Text fontSize="2xl" fontWeight="medium" opacity={0.95}>
大模型辅助的信息整理与呈现平台
AI驱动的概念板块智能分析平台
</Text>
<Text fontSize="lg" opacity={0.8} maxW="3xl" lineHeight="tall">
以大模型协助汇聚与清洗多源信息结合自主训练的领域知识图谱
基于深度学习算法实时监控市场动态精准捕捉概念热点
<br />
并由资深分析师进行人工整合与校准提供结构化参考信息
为您的投资决策提供全方位的数据支持和智能分析
</Text>
</VStack>
</VStack>
@@ -1190,7 +826,7 @@ const ConceptCenter = () => {
<Icon as={FaRocket} boxSize={8} color="cyan.300" />
<Text fontWeight="bold" fontSize="lg">智能追踪</Text>
<Text fontSize="sm" opacity={0.8} textAlign="center">
算法智能追踪
AI算法精准定位
</Text>
</VStack>
@@ -1229,11 +865,11 @@ const ConceptCenter = () => {
<HStack spacing={8} divider={<Divider orientation="vertical" height="40px" borderColor="whiteAlpha.400" />}>
<VStack>
<Text fontSize="3xl" fontWeight="bold" color="yellow.300">500+</Text>
<Text fontSize="3xl" fontWeight="bold" color="yellow.300">{totalConcepts}</Text>
<Text fontSize="sm" opacity={0.8}>概念板块</Text>
</VStack>
<VStack>
<Text fontSize="3xl" fontWeight="bold" color="cyan.300">5000+</Text>
<Text fontSize="3xl" fontWeight="bold" color="cyan.300">10K+</Text>
<Text fontSize="sm" opacity={0.8}>相关个股</Text>
</VStack>
<VStack>
@@ -1323,199 +959,146 @@ const ConceptCenter = () => {
<DateSelector />
</Box>
{/* 双栏布局:左侧概念卡片,右侧统计面板 */}
<Flex gap={8} direction={{ base: 'column', xl: 'row' }}>
{/* 左侧概念卡片区域 */}
<Box flex={1}>
<Card mb={8} shadow="sm">
<CardBody>
<Flex
direction={{ base: 'column', md: 'row' }}
justify="space-between"
align={{ base: 'stretch', md: 'center' }}
gap={4}
<Card mb={8} shadow="sm">
<CardBody>
<Flex
direction={{ base: 'column', md: 'row' }}
justify="space-between"
align={{ base: 'stretch', md: 'center' }}
gap={4}
>
<HStack spacing={4} align="center">
<Text fontWeight="medium" color="gray.700">排序方式</Text>
<Select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value)}
width="200px"
focusBorderColor="purple.500"
>
<HStack spacing={4} align="center">
<Text fontWeight="medium" color="gray.700">排序方式</Text>
<Select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value)}
width="200px"
focusBorderColor="purple.500"
>
<option value="change_pct">涨跌幅</option>
<option value="_score">相关度</option>
<option value="stock_count">股票数量</option>
<option value="concept_name">概念名称</option>
</Select>
{searchQuery && sortBy === '_score' && (
<Tooltip label="搜索时自动切换到相关度排序,以显示最匹配的结果。您也可以手动切换其他排序方式。">
<HStack spacing={1}>
<Icon as={InfoIcon} color="blue.500" boxSize={4} />
<Text fontSize="sm" color="blue.600">
智能排序
</Text>
</HStack>
</Tooltip>
)}
</HStack>
<ButtonGroup size="sm" isAttached variant="outline">
<IconButton
icon={<FaThLarge />}
onClick={() => setViewMode('grid')}
bg={viewMode === 'grid' ? 'purple.500' : 'transparent'}
color={viewMode === 'grid' ? 'white' : 'purple.500'}
borderColor="purple.500"
_hover={{ bg: viewMode === 'grid' ? 'purple.600' : 'purple.50' }}
aria-label="网格视图"
/>
<IconButton
icon={<FaList />}
onClick={() => setViewMode('list')}
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
color={viewMode === 'list' ? 'white' : 'purple.500'}
borderColor="purple.500"
_hover={{ bg: viewMode === 'list' ? 'purple.600' : 'purple.50' }}
aria-label="列表视图"
/>
</ButtonGroup>
</Flex>
</CardBody>
</Card>
{selectedDate && (
<Box mb={4} p={3} bg="blue.50" borderRadius="md" borderLeft="4px solid" borderColor="blue.500">
<HStack>
<Icon as={InfoIcon} color="blue.500" />
<Text fontSize="sm" color="blue.700">
当前显示 <strong>{selectedDate.toLocaleDateString('zh-CN')}</strong>
{searchQuery && <span>搜索词<strong>"{searchQuery}"</strong></span>}
</Text>
</HStack>
</Box>
)}
{loading ? (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
{[...Array(12)].map((_, i) => (
<SkeletonCard key={i} />
))}
</SimpleGrid>
) : concepts.length > 0 ? (
<>
{viewMode === 'grid' ? (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6} className="concept-grid">
{concepts.map((concept) => (
<Box key={concept.concept_id} className="concept-item" role="group">
<ConceptCard concept={concept} />
</Box>
))}
</SimpleGrid>
) : (
<VStack spacing={4} align="stretch" className="concept-list">
{concepts.map((concept) => (
<ConceptListItem key={concept.concept_id} concept={concept} />
))}
</VStack>
)}
<Center mt={12}>
<HStack spacing={2}>
<Button
size="sm"
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
isDisabled={currentPage === 1}
colorScheme="purple"
variant="outline"
>
上一页
</Button>
<HStack>
{[...Array(Math.min(5, totalPages))].map((_, i) => {
const pageNum = currentPage <= 3 ? i + 1 :
currentPage >= totalPages - 2 ? totalPages - 4 + i :
currentPage - 2 + i;
if (pageNum < 1 || pageNum > totalPages) return null;
return (
<Button
key={pageNum}
size="sm"
onClick={() => handlePageChange(pageNum)}
colorScheme="purple"
variant={pageNum === currentPage ? 'solid' : 'outline'}
>
{pageNum}
</Button>
);
})}
<option value="change_pct">涨跌幅</option>
<option value="_score">相关度</option>
<option value="stock_count">股票数量</option>
<option value="concept_name">概念名称</option>
</Select>
{searchQuery && sortBy === '_score' && (
<Tooltip label="搜索时自动切换到相关度排序,以显示最匹配的结果。您也可以手动切换其他排序方式。">
<HStack spacing={1}>
<Icon as={InfoIcon} color="blue.500" boxSize={4} />
<Text fontSize="sm" color="blue.600">
智能排序
</Text>
</HStack>
</Tooltip>
)}
</HStack>
<Button
size="sm"
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}
isDisabled={currentPage === totalPages}
colorScheme="purple"
variant="outline"
>
下一页
</Button>
</HStack>
</Center>
</>
) : (
<Center h="400px">
<VStack spacing={6}>
<Icon as={FaTags} boxSize={20} color="gray.300" />
<VStack spacing={2}>
<Text fontSize="xl" color="gray.600" fontWeight="medium">暂无概念数据</Text>
<Text color="gray.500">请尝试其他搜索关键词或选择其他日期</Text>
</VStack>
</VStack>
</Center>
)}
</Box>
{/* 右侧统计面板 */}
<Box w={{ base: '100%', xl: '400px' }} flexShrink={0}>
<Box position="sticky" top={6}>
{hasFeatureAccess('concept_stats_panel') ? (
<ConceptStatsPanel
apiBaseUrl={API_BASE_URL}
onConceptClick={handleConceptClick}
<ButtonGroup size="sm" isAttached variant="outline">
<IconButton
icon={<FaThLarge />}
onClick={() => setViewMode('grid')}
bg={viewMode === 'grid' ? 'purple.500' : 'transparent'}
color={viewMode === 'grid' ? 'white' : 'purple.500'}
borderColor="purple.500"
_hover={{ bg: viewMode === 'grid' ? 'purple.600' : 'purple.50' }}
aria-label="网格视图"
/>
) : (
<Card>
<CardBody p={6}>
<VStack spacing={4} textAlign="center">
<Icon as={FaChartLine} boxSize={12} color="gray.300" />
<VStack spacing={2}>
<Heading size="md" color="gray.600">
概念统计中心
</Heading>
<Text fontSize="sm" color="gray.500">
此功能需要Pro版订阅才能使用
</Text>
</VStack>
<Button
colorScheme="blue"
leftIcon={<Icon as={FaRocket} />}
onClick={() => {
setUpgradeFeature('pro');
setUpgradeModalOpen(true);
}}
>
升级到Pro版
</Button>
</VStack>
</CardBody>
</Card>
)}
</Box>
<IconButton
icon={<FaList />}
onClick={() => setViewMode('list')}
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
color={viewMode === 'list' ? 'white' : 'purple.500'}
borderColor="purple.500"
_hover={{ bg: viewMode === 'list' ? 'purple.600' : 'purple.50' }}
aria-label="列表视图"
/>
</ButtonGroup>
</Flex>
</CardBody>
</Card>
{selectedDate && (
<Box mb={4} p={3} bg="blue.50" borderRadius="md" borderLeft="4px solid" borderColor="blue.500">
<HStack>
<Icon as={InfoIcon} color="blue.500" />
<Text fontSize="sm" color="blue.700">
当前显示 <strong>{selectedDate.toLocaleDateString('zh-CN')}</strong>
{searchQuery && <span>搜索词<strong>"{searchQuery}"</strong></span>}
</Text>
</HStack>
</Box>
</Flex>
)}
{loading ? (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8}>
{[...Array(12)].map((_, i) => (
<SkeletonCard key={i} />
))}
</SimpleGrid>
) : concepts.length > 0 ? (
<>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} className="concept-grid">
{concepts.map((concept) => (
<Box key={concept.concept_id} className="concept-item" role="group">
<ConceptCard concept={concept} />
</Box>
))}
</SimpleGrid>
<Center mt={12}>
<HStack spacing={2}>
<Button
size="sm"
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
isDisabled={currentPage === 1}
colorScheme="purple"
variant="outline"
>
上一页
</Button>
<HStack>
{[...Array(Math.min(5, totalPages))].map((_, i) => {
const pageNum = currentPage <= 3 ? i + 1 :
currentPage >= totalPages - 2 ? totalPages - 4 + i :
currentPage - 2 + i;
if (pageNum < 1 || pageNum > totalPages) return null;
return (
<Button
key={pageNum}
size="sm"
onClick={() => handlePageChange(pageNum)}
colorScheme="purple"
variant={pageNum === currentPage ? 'solid' : 'outline'}
>
{pageNum}
</Button>
);
})}
</HStack>
<Button
size="sm"
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}
isDisabled={currentPage === totalPages}
colorScheme="purple"
variant="outline"
>
下一页
</Button>
</HStack>
</Center>
</>
) : (
<Center h="400px">
<VStack spacing={6}>
<Icon as={FaTags} boxSize={20} color="gray.300" />
<VStack spacing={2}>
<Text fontSize="xl" color="gray.600" fontWeight="medium">暂无概念数据</Text>
<Text color="gray.500">请尝试其他搜索关键词或选择其他日期</Text>
</VStack>
</VStack>
</Center>
)}
</Container>
{/* 股票详情Modal */}
@@ -1552,16 +1135,6 @@ const ConceptCenter = () => {
conceptId={selectedConceptId}
/>
{/* 订阅升级Modal */}
<SubscriptionUpgradeModal
isOpen={upgradeModalOpen}
onClose={() => setUpgradeModalOpen(false)}
requiredLevel={upgradeFeature}
featureName={
upgradeFeature === 'pro' ? '概念统计中心和热门个股' : '概念历史时间轴'
}
/>
</Box>
);
};