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 层 */}
{/* 高光效果 */}
{/* 左上角涨跌幅 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 ;
})()}
}
bg="purple.500"
color="white"
variant="solid"
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id, concept.stocks)}
borderRadius="full"
px={{ base: 2, md: 4 }}
fontWeight="medium"
boxShadow="0 4px 12px rgba(139, 92, 246, 0.4)"
_hover={{
bg: 'purple.400',
transform: 'scale(1.05)',
boxShadow: '0 6px 16px rgba(139, 92, 246, 0.5)',
}}
transition="all 0.2s"
>
时间轴
{/* 顶部发光条 */}
);
};
// 概念列表项组件 - 列表视图(深色主题版)
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;
})()}
{/* 右侧操作区域 */}
}
bg="whiteAlpha.100"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
onClick={(e) => handleViewStocks(e, concept)}
borderRadius="full"
_hover={{ bg: 'whiteAlpha.200', borderColor: 'whiteAlpha.300' }}
>
查看个股
}
bg="purple.500"
color="white"
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id, concept.stocks)}
borderRadius="full"
boxShadow="0 4px 12px rgba(139, 92, 246, 0.4)"
_hover={{ bg: 'purple.400', boxShadow: '0 6px 16px rgba(139, 92, 246, 0.5)' }}
>
历史时间轴
{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}
/>
)}
}
_hover={{
bgGradient: 'linear(135deg, #5568d3 0%, #663a8e 100%)',
transform: 'scale(1.02)',
}}
_active={{
bgGradient: 'linear(135deg, #4a5abf 0%, #58327a 100%)',
transform: 'scale(0.98)',
}}
onClick={handleSearch}
isLoading={loading}
loadingText="搜索中"
px={8}
minW="140px"
fontWeight="bold"
fontSize="md"
transition="all 0.2s"
border="none"
alignSelf="stretch"
boxShadow="inset 0 1px 0 rgba(255, 255, 255, 0.2)"
>
搜索
{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版订阅才能使用
}
onClick={() => {
setUpgradeFeature('pro');
setUpgradeModalOpen(true);
}}
_hover={{ bg: 'blue.400', boxShadow: '0 0 15px rgba(59, 130, 246, 0.5)' }}
>
升级到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;