import React, { useState, useEffect, useCallback } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { logger } from '../../utils/logger'; import defaultEventImage from '../../assets/img/default-event.jpg'; import { Box, Heading, Text, Input, InputGroup, InputLeftElement, Button, SimpleGrid, Card, CardBody, Image, Badge, Stack, HStack, VStack, Flex, Spacer, Select, Tag, TagLabel, Wrap, WrapItem, useToast, Spinner, Center, Icon, useColorModeValue, IconButton, ButtonGroup, Skeleton, SkeletonText, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Tooltip, Stat, StatLabel, StatNumber, StatHelpText, StatArrow, Divider, Popover, PopoverTrigger, PopoverContent, PopoverBody, PopoverArrow, PopoverCloseButton, Tabs, TabList, TabPanels, Tab, TabPanel, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon, useDisclosure, Menu, MenuButton, MenuList, MenuItem, Collapse, useBreakpointValue, } from '@chakra-ui/react'; import { Search, Eye, Calendar, ExternalLink, Star, ChevronDown, Info, X, ChevronRight, LayoutGrid, List, Tags, LineChart, Bot, Table2, History, Brain, Lightbulb, Rocket, Shield, CalendarDays, ArrowUp, ArrowDown, Newspaper, FileText, Maximize2, Minimize2, Clock, Lock, GitBranch, Layers, Box as BoxIcon, Network, TrendingUp, Zap, } from 'lucide-react'; import { keyframes } from '@emotion/react'; import ConceptTimelineModal from './ConceptTimelineModal'; import ConceptStatsPanel from './components/ConceptStatsPanel'; import HierarchyView from './components/HierarchyView'; import ForceGraphView from './components/ForceGraphView'; import BreadcrumbNav from './components/BreadcrumbNav'; import ConceptStocksModal from '@components/ConceptStocksModal'; import TradeDatePicker from '@components/TradeDatePicker'; // 导航栏已由 MainLayout 提供,无需在此导入 // 导入订阅权限管理 import { useSubscription } from '../../hooks/useSubscription'; import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal'; // 导入市场服务 import { marketService } from '../../services/marketService'; // 导入 PostHog 追踪 Hook import { useConceptEvents } from './hooks/useConceptEvents'; import { getApiBase } from '@utils/apiConfig'; // API配置 - 生产环境通过 api.valuefrontier.cn 代理 const API_BASE_URL = process.env.NODE_ENV === 'production' ? `${getApiBase()}/concept-api` : 'http://111.198.58.126:16801'; // 新闻和研报API配置 const NEWS_API_URL = process.env.NODE_ENV === 'production' ? `${getApiBase()}/news-api` : 'http://111.198.58.126:21891'; const REPORT_API_URL = process.env.NODE_ENV === 'production' ? `${getApiBase()}/report-api` : 'http://111.198.58.126:8811'; // 渐变动画 const gradientAnimation = keyframes` 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } `; // 浮动动画 const floatAnimation = keyframes` 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-20px); } `; // 涨跌幅脉冲动画 const pulseAnimation = keyframes` 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } `; // K线流动动画 - 模拟股票走势 const klineFlowAnimation = keyframes` 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } `; // 网格扫描动画 const gridScanAnimation = keyframes` 0% { opacity: 0.3; transform: translateY(0); } 50% { opacity: 0.6; transform: translateY(-20px); } 100% { opacity: 0.3; transform: translateY(0); } `; // 脉冲光点动画 const pulseGlowAnimation = keyframes` 0%, 100% { opacity: 0.4; transform: scale(1); } 50% { opacity: 0.8; transform: scale(1.2); } `; /** * 金融数据流动背景组件 - 模拟K线走势 */ const FinanceFlowBackground = () => ( {/* 深色渐变底层 */} {/* 网格线背景 */} {/* K线流动效果 - 多条线 */} {[...Array(6)].map((_, i) => ( ))} {/* 垂直扫描线 */} {/* 脉冲光点 */} {[ { top: '20%', left: '15%', color: 'rgba(239, 68, 68, 0.6)', delay: '0s' }, { top: '40%', left: '75%', color: 'rgba(34, 197, 94, 0.6)', delay: '1s' }, { top: '60%', left: '30%', color: 'rgba(139, 92, 246, 0.6)', delay: '2s' }, { top: '80%', left: '60%', color: 'rgba(6, 182, 212, 0.6)', delay: '3s' }, { top: '30%', left: '90%', color: 'rgba(239, 68, 68, 0.5)', delay: '0.5s' }, { top: '70%', left: '10%', color: 'rgba(34, 197, 94, 0.5)', delay: '1.5s' }, ].map((dot, i) => ( ))} {/* 顶部渐变遮罩 */} {/* 底部渐变遮罩 */} ); const ConceptCenter = () => { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const toast = useToast(); // 🎯 PostHog 事件追踪 const { trackConceptSearched, trackFilterApplied, trackConceptClicked, trackConceptStocksViewed, trackConceptStockClicked, trackConceptTimelineViewed, trackPageChange, trackViewModeChanged, } = useConceptEvents({ navigate }); // 订阅权限管理 const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription(); const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); const [upgradeFeature, setUpgradeFeature] = useState('pro'); // 状态管理 const [concepts, setConcepts] = useState([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('change_pct'); const [pageSize] = useState(12); const [currentPage, setCurrentPage] = useState(1); const [totalConcepts, setTotalConcepts] = useState(0); const [totalPages, setTotalPages] = useState(1); const [viewMode, setViewMode] = useState('list'); // 默认列表视图 // 层级筛选状态 const [hierarchyFilter, setHierarchyFilter] = useState({ lv1: null, lv2: null, lv3: null }); // 日期相关状态 const [selectedDate, setSelectedDate] = useState(null); const [latestTradeDate, setLatestTradeDate] = useState(null); const [isDatePickerOpen, setIsDatePickerOpen] = useState(false); const [selectedConceptForContent, setSelectedConceptForContent] = useState(''); // 股票详情Modal const [isStockModalOpen, setIsStockModalOpen] = useState(false); const [selectedConceptStocks, setSelectedConceptStocks] = useState(null); const [selectedConceptName, setSelectedConceptName] = useState(''); const [isTimelineModalOpen, setIsTimelineModalOpen] = useState(false); const [selectedConceptId, setSelectedConceptId] = useState(''); const [selectedConceptStocksForTimeline, setSelectedConceptStocksForTimeline] = useState([]); // 股票行情数据状态 const [stockMarketData, setStockMarketData] = useState({}); const [loadingStockData, setLoadingStockData] = useState(false); // 默认图片路径 const defaultImage = defaultEventImage; // 获取最新交易日期 const fetchLatestTradeDate = useCallback(async () => { try { const response = await fetch(`${API_BASE_URL}/price/latest`); if (response.ok) { const data = await response.json(); if (data.latest_trade_date) { const date = new Date(data.latest_trade_date); setLatestTradeDate(date); setSelectedDate(date); return date; } } } catch (error) { logger.error('ConceptCenter', 'fetchLatestTradeDate', error); } return null; }, []); // 打开内容模态框(新闻和研报)- 需要Max版权限 const handleViewContent = (e, conceptName, conceptId, stocks = []) => { e.stopPropagation(); // 检查历史时间轴权限 if (!hasFeatureAccess('concept_timeline')) { const recommendation = getUpgradeRecommendation('concept_timeline'); setUpgradeFeature(recommendation?.required || 'max'); setUpgradeModalOpen(true); return; } // 🎯 追踪历史时间轴查看 trackConceptTimelineViewed(conceptName, conceptId); setSelectedConceptForContent(conceptName); setSelectedConceptId(conceptId); setSelectedConceptStocksForTimeline(stocks || []); setIsTimelineModalOpen(true); }; // 格式化日期 const formatDate = (dateStr) => { if (!dateStr) return '未知日期'; try { const date = new Date(dateStr); return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch { return dateStr; } }; // 截取文本 const truncateText = (text, maxLength = 200) => { if (!text) return ''; if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; }; // 从URL获取参数 const getFiltersFromUrl = useCallback(() => { const q = searchParams.get('q') || ''; let defaultSort = q ? '_score' : 'change_pct'; return { q: q, sort: searchParams.get('sort') || defaultSort, page: parseInt(searchParams.get('page') || '1', 10), date: searchParams.get('date') || null, size: 12, // 层级筛选参数 lv1: searchParams.get('lv1') || null, lv2: searchParams.get('lv2') || null, lv3: searchParams.get('lv3') || null, }; }, [searchParams]); // 更新URL参数 const updateUrlParams = useCallback((params) => { const newParams = new URLSearchParams(searchParams); Object.entries(params).forEach(([key, value]) => { if (value) { newParams.set(key, value); } else { newParams.delete(key); } }); setSearchParams(newParams); }, [searchParams, setSearchParams]); // 获取概念数据 const fetchConcepts = useCallback(async (query = '', page = 1, date = selectedDate, customSortBy = null, filter = hierarchyFilter) => { setLoading(true); try { const sortToUse = customSortBy !== null ? customSortBy : sortBy; const requestBody = { query: query, size: pageSize, page: page, sort_by: sortToUse }; if (date) { requestBody.trade_date = date.toISOString().split('T')[0]; } // 添加层级筛选参数 if (filter?.lv1) { requestBody.filter_lv1 = filter.lv1; } if (filter?.lv2) { requestBody.filter_lv2 = filter.lv2; } const response = await fetch(`${API_BASE_URL}/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) throw new Error('搜索失败'); const data = await response.json(); setConcepts(data.results || []); setTotalConcepts(data.total || 0); setTotalPages(data.total_pages || 1); setCurrentPage(data.page || 1); if (data.price_date) { setSelectedDate(new Date(data.price_date)); } } catch (error) { logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), sortToUse, filter }); // ❌ 移除获取数据失败toast // toast({ title: '获取数据失败', description: error.message, status: 'error', duration: 3000, isClosable: true }); } finally { setLoading(false); } }, [pageSize, sortBy, hierarchyFilter]); // 清除搜索 const handleClearSearch = () => { setSearchQuery(''); setSortBy('change_pct'); setCurrentPage(1); setHierarchyFilter({ lv1: null, lv2: null, lv3: null }); updateUrlParams({ q: '', page: 1, sort: 'change_pct', lv1: '', lv2: '', lv3: '' }); fetchConcepts('', 1, selectedDate, 'change_pct', { lv1: null, lv2: null, lv3: null }); }; // 处理层级筛选选择(从 HierarchyView 点击分类 - 仅在需要时使用) // 注意:热力图视图现在是独立的钻取交互,不再自动切换到列表视图 const handleHierarchySelect = useCallback((filter) => { logger.info('ConceptCenter', '层级筛选选择', filter); setHierarchyFilter(filter); setCurrentPage(1); // 不再自动切换视图,热力图内部自己处理钻取 // 更新 URL 参数 updateUrlParams({ lv1: filter.lv1 || '', lv2: filter.lv2 || '', lv3: filter.lv3 || '', page: 1 }); // 重新获取数据(用于其他视图) fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter); }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]); // 清除层级筛选 const handleClearHierarchyFilter = useCallback(() => { setHierarchyFilter({ lv1: null, lv2: null, lv3: null }); setCurrentPage(1); updateUrlParams({ lv1: '', lv2: '', lv3: '', page: 1 }); fetchConcepts(searchQuery, 1, selectedDate, sortBy, { lv1: null, lv2: null, lv3: null }); }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]); // 导航到特定层级 const handleNavigateHierarchy = useCallback((filter) => { setHierarchyFilter(filter); setCurrentPage(1); updateUrlParams({ lv1: filter.lv1 || '', lv2: filter.lv2 || '', lv3: filter.lv3 || '', page: 1 }); fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter); }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]); // 处理搜索 const handleSearch = () => { setCurrentPage(1); let newSortBy = sortBy; if (searchQuery && searchQuery.trim() !== '') { newSortBy = '_score'; setSortBy('_score'); } else if (!searchQuery || searchQuery.trim() === '') { newSortBy = 'change_pct'; setSortBy('change_pct'); } // 🎯 追踪搜索查询(在fetchConcepts后追踪结果数量) updateUrlParams({ q: searchQuery, page: 1, sort: newSortBy }); fetchConcepts(searchQuery, 1, selectedDate, newSortBy).then(() => { if (searchQuery && searchQuery.trim() !== '') { // 使用当前 concepts.length 作为结果数量 setTimeout(() => trackConceptSearched(searchQuery, concepts.length), 100); } }); }; // 处理Enter键搜索 const handleKeyPress = (e) => { if (e.key === 'Enter') { handleSearch(); } }; // 处理排序变化 const handleSortChange = (value) => { const previousSort = sortBy; // 🎯 追踪排序变化 trackFilterApplied('sort', value, previousSort); setSortBy(value); setCurrentPage(1); updateUrlParams({ sort: value, page: 1 }); fetchConcepts(searchQuery, 1, selectedDate, value); }; // 处理日期变化 const handleDateChange = (e) => { const date = new Date(e.target.value); const previousDate = selectedDate ? selectedDate.toISOString().split('T')[0] : null; // 🎯 追踪日期变化 trackFilterApplied('date', e.target.value, previousDate); setSelectedDate(date); setCurrentPage(1); updateUrlParams({ date: e.target.value, page: 1 }); fetchConcepts(searchQuery, 1, date, sortBy); }; // 快速选择日期 const handleQuickDateSelect = (days) => { const date = new Date(); date.setDate(date.getDate() - days); setSelectedDate(date); setCurrentPage(1); const dateStr = date.toISOString().split('T')[0]; updateUrlParams({ date: dateStr, page: 1 }); fetchConcepts(searchQuery, 1, date, sortBy); }; // 处理页码变化 const handlePageChange = (page) => { // 🎯 追踪翻页 trackPageChange(page, { sort: sortBy, q: searchQuery, date: selectedDate?.toISOString().split('T')[0] }); setCurrentPage(page); updateUrlParams({ page }); fetchConcepts(searchQuery, page, selectedDate, sortBy); window.scrollTo(0, 0); }; // 处理概念点击 const handleConceptClick = (conceptId, conceptName, concept = null, position = 0) => { // 🎯 追踪概念点击 if (concept) { trackConceptClicked(concept, position); } const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`; window.open(htmlPath, '_blank'); }; // 获取股票行情数据(使用批量接口,减少网络请求) const fetchStockMarketData = async (stocks) => { if (!stocks || stocks.length === 0) return; setLoadingStockData(true); const newMarketData = {}; try { // 提取所有6位股票代码 const stockCodeMap = {}; // seccode -> fullCode 映射 const seccodes = []; stocks.forEach((stock) => { if (stock.stock_code) { const seccode = stock.stock_code.substring(0, 6); stockCodeMap[seccode] = stock.stock_code; seccodes.push(seccode); } }); if (seccodes.length === 0) return; // 使用批量接口一次性获取所有数据 const response = await marketService.getBatchTradeData(seccodes, 1); if (response.success && response.data) { Object.entries(response.data).forEach(([seccode, stockData]) => { const fullCode = stockCodeMap[seccode]; if (fullCode && stockData.data?.length > 0) { const latestData = stockData.data[stockData.data.length - 1]; newMarketData[fullCode] = { stock_code: fullCode, ...latestData }; } }); } setStockMarketData(newMarketData); logger.info('ConceptCenter', '股票行情数据批量加载完成', { totalStocks: stocks.length, loadedCount: Object.keys(newMarketData).length }); } catch (error) { logger.error('ConceptCenter', 'fetchStockMarketData', error, { stockCount: stocks?.length }); } finally { setLoadingStockData(false); } }; // 打开股票详情Modal - 需要Pro版权限 const handleViewStocks = (e, concept) => { e.stopPropagation(); // 检查热门个股权限 if (!hasFeatureAccess('hot_stocks')) { const recommendation = getUpgradeRecommendation('hot_stocks'); setUpgradeFeature(recommendation?.required || 'pro'); setUpgradeModalOpen(true); return; } // 🎯 追踪查看个股 trackConceptStocksViewed(concept.concept, concept.stocks?.length || 0); setSelectedConceptStocks(concept.stocks || []); setSelectedConceptName(concept.concept); setStockMarketData({}); // 清空之前的数据 setIsStockModalOpen(true); // 获取股票行情数据 fetchStockMarketData(concept.stocks || []); }; // 格式化涨跌幅显示 const formatChangePercent = (value) => { if (value === null || value === undefined) return null; const formatted = value.toFixed(2); return formatted > 0 ? `+${formatted}%` : `${formatted}%`; }; // 获取涨跌幅颜色 const getChangeColor = (value) => { if (value === null || value === undefined) return 'gray'; 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}`; }; // 获取最新爆发日期(从 outbreak_dates 数组中取最新的) const getLatestOutbreakDate = (concept) => { const dates = concept.outbreak_dates; if (!dates || dates.length === 0) return null; // 排序获取最新日期 const sortedDates = [...dates].sort((a, b) => new Date(b) - new Date(a)); return sortedDates[0]; }; // 格式化添加日期显示 const formatAddedDate = (concept) => { // 优先使用 created_at 或 added_date 字段 const addedDate = concept.created_at || concept.added_date || concept.happened_times?.[0]; if (!addedDate) return null; return ( 添加于 {new Date(addedDate).toLocaleDateString('zh-CN')} ); }; // 初始化加载 useEffect(() => { const init = async () => { const latestDate = await fetchLatestTradeDate(); const filters = getFiltersFromUrl(); setSearchQuery(filters.q); setSortBy(filters.sort); setCurrentPage(filters.page); // 恢复层级筛选状态 const hierarchyFilterFromUrl = { lv1: filters.lv1, lv2: filters.lv2, lv3: filters.lv3, }; setHierarchyFilter(hierarchyFilterFromUrl); const dateToUse = filters.date ? new Date(filters.date) : latestDate; if (dateToUse) { setSelectedDate(dateToUse); fetchConcepts(filters.q, filters.page, dateToUse, filters.sort, hierarchyFilterFromUrl); } else { fetchConcepts(filters.q, filters.page, null, filters.sort, hierarchyFilterFromUrl); } }; init(); }, []); // 获取股票名称(兼容新旧API格式) const getStockName = (stock) => stock.name || stock.stock_name || '未知'; const getStockCode = (stock) => stock.code || stock.stock_code || ''; // 概念卡片组件 - 深色毛玻璃版(HeroUI风格) const ConceptCard = ({ concept, position = 0 }) => { const changePercent = concept.price_info?.avg_change_pct; const changeColor = getChangeColor(changePercent); const hasChange = changePercent !== null && changePercent !== undefined; // H5 端使用更紧凑的尺寸 const isMobile = useBreakpointValue({ base: true, md: false }); const coverHeight = useBreakpointValue({ base: '100px', md: '160px' }); const logoSize = useBreakpointValue({ base: '60px', md: '100px' }); // 生成随机涨幅数字背景 const generateNumbersBackground = () => { const numbers = []; for (let i = 0; i < 30; i++) { const isPositive = Math.random() > 0.5; const value = (Math.random() * 15).toFixed(2); const sign = isPositive ? '+' : '-'; numbers.push(`${sign}${value}%`); } return numbers; }; const backgroundNumbers = generateNumbersBackground(); return ( handleConceptClick(concept.concept_id, concept.concept, concept, position)} bg="rgba(15, 23, 42, 0.8)" backdropFilter="blur(20px)" borderWidth="1px" borderColor="whiteAlpha.100" overflow="hidden" _hover={{ transform: 'translateY(-6px)', boxShadow: '0 20px 40px rgba(139, 92, 246, 0.25)', borderColor: 'purple.500', }} transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" position="relative" boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)" borderRadius="2xl" > {/* 毛玻璃涨幅数字背景 */} {/* 渐变背景层 - 涨红跌绿 */} 0 ? "linear(135deg, rgba(153, 27, 27, 0.6) 0%, rgba(239, 68, 68, 0.4) 100%)" : hasChange && changePercent < 0 ? "linear(135deg, rgba(20, 83, 45, 0.6) 0%, rgba(34, 197, 94, 0.4) 100%)" : "linear(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)" } /> {/* 数字矩阵层 */} {backgroundNumbers.map((num, idx) => ( {num} ))} {/* 公司 Logo 层 */} Company Logo {/* 高光效果 */} {/* 左上角涨跌幅 Badge */} {hasChange && ( 5 ? `${pulseAnimation} 2s infinite` : 'none'} > 0 ? ArrowUp : changePercent < 0 ? ArrowDown : null} boxSize={3} /> {formatChangePercent(changePercent)} )} {/* 右上角股票数量徽章 - 可点击 */} handleViewStocks(e, concept)} > {concept.stock_count || 0} 只股票 {/* 概念名称 */} {concept.concept} {/* 描述信息 - H5端显示1行 */} {concept.description || '暂无描述信息'} {concept.stocks && concept.stocks.length > 0 && ( handleViewStocks(e, concept)} _hover={{ bg: 'whiteAlpha.100', transform: 'translateX(2px)', }} transition="all 0.2s" border="1px solid" borderColor="whiteAlpha.100" > 热门个股 {!hasFeatureAccess('hot_stocks') && ( 🔒Pro )} {hasFeatureAccess('hot_stocks') ? ( <> {concept.stocks.slice(0, 2).map((stock, idx) => ( { e.stopPropagation(); window.open(generateCompanyLink(getStockCode(stock)), '_blank'); }} > {getStockName(stock)} ))} {concept.stocks.length > 2 && ( +{concept.stocks.length - 2} )} ) : ( 升级查看{concept.stocks.length}只个股 )} )} {/* 日期显示 - 根据排序方式显示爆发日期或添加日期 */} {(() => { const latestOutbreak = getLatestOutbreakDate(concept); const addedDate = concept.created_at || concept.added_date || concept.happened_times?.[0]; // 优先显示爆发日期(如果存在) if (latestOutbreak) { return ( 爆发于 {new Date(latestOutbreak).toLocaleDateString('zh-CN')} ); } else if (addedDate) { return ( 添加于 {new Date(addedDate).toLocaleDateString('zh-CN')} ); } return ; })()} {/* 顶部发光条 */} ); }; // 概念列表项组件 - 列表视图(深色主题版) const ConceptListItem = ({ concept, position = 0 }) => { const changePercent = concept.price_info?.avg_change_pct; const changeColor = getChangeColor(changePercent); const hasChange = changePercent !== null && changePercent !== undefined; return ( handleConceptClick(concept.concept_id, concept.concept, concept, position)} bg="rgba(15, 23, 42, 0.8)" backdropFilter="blur(20px)" borderWidth="1px" borderColor="whiteAlpha.100" overflow="hidden" borderRadius="2xl" _hover={{ transform: 'translateX(4px)', boxShadow: '0 8px 32px rgba(139, 92, 246, 0.25)', borderColor: 'purple.500', }} transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" > {/* 左侧图标区域 */} 0 ? "linear-gradient(135deg, rgba(153, 27, 27, 0.6) 0%, rgba(239, 68, 68, 0.4) 100%)" : changePercent < 0 ? "linear-gradient(135deg, rgba(20, 83, 45, 0.6) 0%, rgba(34, 197, 94, 0.4) 100%)" : "linear-gradient(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)" : "linear-gradient(135deg, rgba(99, 102, 241, 0.4) 0%, rgba(168, 85, 247, 0.3) 100%)" } display="flex" alignItems="center" justifyContent="center" position="relative" flexShrink={0} border="1px solid" borderColor="whiteAlpha.100" > {hasChange && ( 0 ? ArrowUp : changePercent < 0 ? ArrowDown : null} boxSize={2} mr={1} /> {formatChangePercent(changePercent)} )} {/* 中间内容区域 */} {concept.concept} {concept.description || '该概念板块涵盖相关技术、产业链和市场应用等多个维度的投资机会'} handleViewStocks(e, concept)} _hover={{ color: 'purple.300' }} transition="color 0.2s" > {concept.stock_count || 0} 只股票 {hasChange && concept.price_info?.trade_date && ( {new Date(concept.price_info.trade_date).toLocaleDateString('zh-CN')} )} {/* 日期显示 - 优先显示爆发日期 */} {(() => { const latestOutbreak = getLatestOutbreakDate(concept); const addedDate = concept.created_at || concept.added_date || concept.happened_times?.[0]; if (latestOutbreak) { return ( 爆发于 {new Date(latestOutbreak).toLocaleDateString('zh-CN')} ); } else if (addedDate) { return ( 添加于 {new Date(addedDate).toLocaleDateString('zh-CN')} ); } return null; })()} {/* 右侧操作区域 */} {concept.stocks && concept.stocks.length > 0 && ( 热门个股 {!hasFeatureAccess('hot_stocks') && ( 🔒需Pro )} {hasFeatureAccess('hot_stocks') ? ( <> {concept.stocks.slice(0, 3).map((stock, idx) => ( { e.stopPropagation(); window.open(generateCompanyLink(getStockCode(stock)), '_blank'); }} > {getStockName(stock)} ))} {concept.stocks.length > 3 && ( handleViewStocks(e, concept)} > +{concept.stocks.length - 3}更多 )} ) : ( 升级查看{concept.stocks.length}只 )} )} ); }; // 骨架屏组件 - 深色主题 const SkeletonCard = () => ( ); // 日期选择组件 - 深色主题 const DateSelector = () => ( {/* 使用通用日期选择器组件 - 不显示最新日期提示,由下方单独渲染 */} { 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); }} latestTradeDate={latestTradeDate} label="交易日期" isDarkMode={true} showLatestTradeDateTip={false} /> {/* 快捷按钮 - 紧跟日期选择器 */} {/* 最新交易日期提示 - 靠右显示 */} {latestTradeDate && ( 数据更新至 {latestTradeDate.toLocaleDateString('zh-CN')} )} ); return ( {/* 金融数据流动动画背景 */} {/* 导航栏已由 MainLayout 提供 */} {/* Hero Section - 精简版 */} {/* Hero Section - 使用负 margin 抵消 Layout 的 padding 实现全宽背景 */} {/* 科幻网格背景 */} {/* 发光球体 */} {/* 标题区域 */} 概念中心 数据约下午4点更新 AI驱动的概念板块分析平台 · 实时追踪市场热点 · 智能挖掘投资机会 {/* 核心数据展示 */} } bg="whiteAlpha.100" backdropFilter="blur(10px)" px={8} py={3} borderRadius="full" border="1px solid" borderColor="whiteAlpha.300" boxShadow="0 8px 32px rgba(0, 0, 0, 0.3)" > 500+ 概念板块 5000+ 相关个股 24/7 实时监控 {/* 搜索框 */} setSearchQuery(e.target.value)} onKeyPress={handleKeyPress} pr={searchQuery ? "50px" : "16px"} /> {searchQuery && ( } variant="ghost" color="gray.500" borderRadius="full" _hover={{ color: 'gray.700', bg: 'gray.200' }} onClick={handleClearSearch} zIndex={1} /> )} {searchQuery && sortBy === '_score' && ( 正在搜索 "{searchQuery}",已自动切换到相关度排序 )} {/* 主内容区域 - padding 由 MainLayout 统一设置 */} {/* 双栏布局:左侧概念卡片,右侧统计面板 */} {/* 左侧概念卡片区域 */} {/* 排序方式 - 仅在列表视图显示 */} {viewMode === 'list' && ( 排序方式: {searchQuery && sortBy === '_score' && ( 智能排序 )} )} } 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="概念矩形树图" /> } 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="层级图" /> } 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="列表视图" /> {/* 面包屑导航 - 显示当前层级筛选 */} {selectedDate && viewMode !== 'hierarchy' && viewMode !== 'force3d' && ( 当前显示 {selectedDate.toLocaleDateString('zh-CN')} 的概念涨跌幅数据 {searchQuery && ,搜索词:"{searchQuery}"} )} {/* 3D 力导向图视图 */} {viewMode === 'force3d' ? ( ) : /* 层级图视图 */ viewMode === 'hierarchy' ? ( ) : loading ? ( {[...Array(12)].map((_, i) => ( ))} ) : concepts.length > 0 ? ( <> {viewMode === 'grid' ? ( {concepts.map((concept, index) => ( ))} ) : ( {concepts.map((concept, index) => ( ))} )}
{[...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 ( ); })}
) : viewMode !== 'hierarchy' && viewMode !== 'force3d' ? (
暂无概念数据 {hierarchyFilter?.lv1 ? `「${[hierarchyFilter.lv1, hierarchyFilter.lv2, hierarchyFilter.lv3].filter(Boolean).join(' > ')}」分类下暂无数据` : '请尝试其他搜索关键词或选择其他日期' } {hierarchyFilter?.lv1 && ( )}
) : null}
{/* 右侧统计面板 */} {hasFeatureAccess('concept_stats_panel') ? ( ) : ( 概念统计中心 此功能需要Pro版订阅才能使用 )}
{/* 股票详情Modal - 复用通用组件 */} setIsStockModalOpen(false)} concept={{ concept_name: selectedConceptName, stocks: selectedConceptStocks }} /> {/* 时间轴Modal */} setIsTimelineModalOpen(false)} conceptName={selectedConceptForContent} conceptId={selectedConceptId} stocks={selectedConceptStocksForTimeline} /> {/* 订阅升级Modal */} setUpgradeModalOpen(false)} requiredLevel={upgradeFeature} featureName={ upgradeFeature === 'pro' ? '概念统计中心和热门个股' : '概念历史时间轴' } />
); }; export default ConceptCenter;