feat: 10.10线上最新代码提交
This commit is contained in:
@@ -122,14 +122,13 @@ const ConceptTimelineModal = ({
|
||||
})
|
||||
);
|
||||
|
||||
// 获取新闻(使用与原代码相同的参数)
|
||||
// 获取新闻(精确匹配,最近100天,最多100条)
|
||||
const newsParams = new URLSearchParams({
|
||||
query: conceptName,
|
||||
exact_match: 1,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
top_k: 100, // 与原代码一致
|
||||
pagenum: 1, // 第一页
|
||||
pagesize: 100 // 一次获取100条,避免分页
|
||||
top_k: 100
|
||||
});
|
||||
|
||||
const newsUrl = `${NEWS_API_URL}/search_china_news?${newsParams}`;
|
||||
@@ -150,14 +149,13 @@ const ConceptTimelineModal = ({
|
||||
})
|
||||
);
|
||||
|
||||
// 获取研报(使用与原代码相同的参数结构)
|
||||
// 获取研报(文本模式、精确匹配,最近100天,最多30条)
|
||||
const reportParams = new URLSearchParams({
|
||||
query: conceptName,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
size: 100, // 一次获取100条
|
||||
from: 0, // 从第0条开始
|
||||
mode: 'hybrid' // 与原代码一致
|
||||
mode: 'text',
|
||||
exact_match: 1,
|
||||
size: 30,
|
||||
start_date: startDateStr
|
||||
});
|
||||
|
||||
const reportUrl = `${REPORT_API_URL}/search?${reportParams}`;
|
||||
@@ -235,34 +233,41 @@ const ConceptTimelineModal = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 处理研报(按时间降序排序,最新的在前)
|
||||
if (reportResult && reportResult.results && Array.isArray(reportResult.results)) {
|
||||
// 先排序
|
||||
const sortedReports = reportResult.results.sort((a, b) => {
|
||||
const dateA = new Date(a.declare_date || 0);
|
||||
const dateB = new Date(b.declare_date || 0);
|
||||
return dateB - dateA; // 降序
|
||||
});
|
||||
// 处理研报(按时间降序排序,最新的在前),兼容 data.results 与 results
|
||||
if (reportResult) {
|
||||
const reports = (reportResult.data && Array.isArray(reportResult.data.results))
|
||||
? reportResult.data.results
|
||||
: (Array.isArray(reportResult.results) ? reportResult.results : []);
|
||||
|
||||
sortedReports.forEach(report => {
|
||||
if (report.declare_date) {
|
||||
// 研报日期格式已经是 YYYY-MM-DD
|
||||
const dateOnly = report.declare_date;
|
||||
if (reports.length > 0) {
|
||||
const sortedReports = reports.sort((a, b) => {
|
||||
const dateA = new Date((a.declare_date || '').replace(' ', 'T'));
|
||||
const dateB = new Date((b.declare_date || '').replace(' ', 'T'));
|
||||
return dateB - dateA; // 降序
|
||||
});
|
||||
|
||||
events.push({
|
||||
type: 'report',
|
||||
date: dateOnly, // 日期
|
||||
time: report.declare_date, // 研报没有具体时间
|
||||
title: report.report_title,
|
||||
content: report.content,
|
||||
publisher: report.publisher,
|
||||
author: report.author,
|
||||
rating: report.rating,
|
||||
security_name: report.security_name,
|
||||
content_url: report.content_url
|
||||
});
|
||||
}
|
||||
});
|
||||
sortedReports.forEach(report => {
|
||||
const rawDate = report.declare_date || '';
|
||||
if (rawDate) {
|
||||
const dateOnly = rawDate.includes('T') ? rawDate.split('T')[0]
|
||||
: rawDate.includes(' ') ? rawDate.split(' ')[0]
|
||||
: rawDate;
|
||||
|
||||
events.push({
|
||||
type: 'report',
|
||||
date: dateOnly,
|
||||
time: rawDate,
|
||||
title: report.report_title,
|
||||
content: report.content,
|
||||
publisher: report.publisher,
|
||||
author: report.author,
|
||||
rating: report.rating,
|
||||
security_name: report.security_name,
|
||||
content_url: report.content_url
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
@@ -321,18 +326,17 @@ const ConceptTimelineModal = ({
|
||||
}));
|
||||
};
|
||||
|
||||
// 格式化日期显示
|
||||
// 格式化日期显示(包含年份)
|
||||
const formatDateDisplay = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
const today = new Date();
|
||||
const diffTime = today - date;
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
const formatted = date.toLocaleDateString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short'
|
||||
});
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const formatted = `${year}-${month}-${day}`;
|
||||
|
||||
if (diffDays === 0) return `今天 ${formatted}`;
|
||||
if (diffDays === 1) return `昨天 ${formatted}`;
|
||||
@@ -342,6 +346,20 @@ const ConceptTimelineModal = ({
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// 格式化完整时间(YYYY-MM-DD HH:mm)
|
||||
const formatDateTime = (dateTimeStr) => {
|
||||
if (!dateTimeStr) return '-';
|
||||
const normalized = typeof dateTimeStr === 'string' ? dateTimeStr.replace(' ', 'T') : dateTimeStr;
|
||||
const dt = new Date(normalized);
|
||||
if (isNaN(dt.getTime())) return '-';
|
||||
const y = dt.getFullYear();
|
||||
const m = String(dt.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(dt.getDate()).padStart(2, '0');
|
||||
const hh = String(dt.getHours()).padStart(2, '0');
|
||||
const mm = String(dt.getMinutes()).padStart(2, '0');
|
||||
return `${y}-${m}-${d} ${hh}:${mm}`;
|
||||
};
|
||||
|
||||
// 获取涨跌幅颜色和图标
|
||||
const getPriceInfo = (price) => {
|
||||
if (!price || price.avg_change_pct === null) {
|
||||
@@ -393,11 +411,33 @@ const ConceptTimelineModal = ({
|
||||
<Badge colorScheme="yellow" ml={2}>
|
||||
最近100天
|
||||
</Badge>
|
||||
<Badge colorScheme="purple" ml={2} fontSize="xs">
|
||||
🔥 Max版功能
|
||||
</Badge>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color="white" />
|
||||
|
||||
<ModalBody py={6} bg="gray.50">
|
||||
<ModalBody
|
||||
py={6}
|
||||
bg="gray.50"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: '#f1f1f1',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#c1c1c1',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: '#a8a8a8',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Center py={20}>
|
||||
<VStack spacing={4}>
|
||||
@@ -614,6 +654,22 @@ const ConceptTimelineModal = ({
|
||||
boxShadow="sm"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '6px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: '#f1f1f1',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#c1c1c1',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: '#a8a8a8',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.events.map((event, eventIdx) => (
|
||||
<Box
|
||||
@@ -667,17 +723,38 @@ const ConceptTimelineModal = ({
|
||||
{event.content || '暂无内容'}
|
||||
</Text>
|
||||
|
||||
{event.url && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="link"
|
||||
colorScheme="blue"
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
onClick={() => window.open(event.url, '_blank')}
|
||||
>
|
||||
查看原文
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="link"
|
||||
colorScheme="blue"
|
||||
leftIcon={<ViewIcon />}
|
||||
onClick={() => {
|
||||
if (event.type === 'news') {
|
||||
setSelectedNews({
|
||||
title: event.title,
|
||||
content: event.content,
|
||||
source: event.source,
|
||||
time: event.time,
|
||||
url: event.url
|
||||
});
|
||||
setIsNewsModalOpen(true);
|
||||
} else if (event.type === 'report') {
|
||||
setSelectedReport({
|
||||
title: event.title,
|
||||
content: event.content,
|
||||
publisher: event.publisher,
|
||||
author: event.author,
|
||||
time: event.time,
|
||||
rating: event.rating,
|
||||
security_name: event.security_name,
|
||||
content_url: event.content_url
|
||||
});
|
||||
setIsReportModalOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
))}
|
||||
@@ -757,7 +834,7 @@ const ConceptTimelineModal = ({
|
||||
<Text>{selectedReport.author}</Text>
|
||||
)}
|
||||
{selectedReport?.time && (
|
||||
<Text>{selectedReport.time}</Text>
|
||||
<Text>{formatDateTime(selectedReport.time)}</Text>
|
||||
)}
|
||||
{selectedReport?.rating && (
|
||||
<Badge colorScheme="orange" variant="solid">
|
||||
@@ -835,17 +912,8 @@ const ConceptTimelineModal = ({
|
||||
{selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedNews?.time && selectedNews.time.includes('T') && (
|
||||
<Text>
|
||||
{new Date(selectedNews.time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
</Text>
|
||||
{selectedNews?.time && (
|
||||
<Text>{formatDateTime(selectedNews.time)}</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
@@ -78,12 +78,18 @@ 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 } from 'react-icons/fa';
|
||||
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 { 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'
|
||||
@@ -123,6 +129,11 @@ 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);
|
||||
@@ -145,6 +156,9 @@ 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';
|
||||
|
||||
@@ -166,9 +180,18 @@ 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);
|
||||
@@ -347,16 +370,78 @@ const ConceptCenter = () => {
|
||||
|
||||
// 处理概念点击
|
||||
const handleConceptClick = (conceptId, conceptName) => {
|
||||
const htmlPath = `/htmls/${conceptName}.html`;
|
||||
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`;
|
||||
window.open(htmlPath, '_blank');
|
||||
};
|
||||
|
||||
// 打开股票详情Modal
|
||||
// 获取股票行情数据
|
||||
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版权限
|
||||
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 || []);
|
||||
};
|
||||
|
||||
// 格式化涨跌幅显示
|
||||
@@ -372,6 +457,33 @@ 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) {
|
||||
@@ -383,7 +495,8 @@ const ConceptCenter = () => {
|
||||
Object.keys(stock).forEach(key => allFields.add(key));
|
||||
});
|
||||
|
||||
const orderedFields = ['stock_name', 'stock_code'];
|
||||
// 定义固定的列顺序,包含新增的现价和涨跌幅列
|
||||
const orderedFields = ['stock_name', 'stock_code', 'current_price', 'change_percent'];
|
||||
allFields.forEach(field => {
|
||||
if (!orderedFields.includes(field)) {
|
||||
orderedFields.push(field);
|
||||
@@ -391,31 +504,86 @@ const ConceptCenter = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<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' }}>
|
||||
<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>
|
||||
{orderedFields.map(field => (
|
||||
<Td key={field}>
|
||||
{stock[field] || '-'}
|
||||
</Td>
|
||||
<Th key={field}>
|
||||
{field === 'stock_name' ? '股票名称' :
|
||||
field === 'stock_code' ? '股票代码' :
|
||||
field === 'current_price' ? '现价' :
|
||||
field === 'change_percent' ? '当日涨跌幅' : field}
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -608,23 +776,45 @@ const ConceptCenter = () => {
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Box flex={1}>
|
||||
<Text fontSize="xs" color="gray.600" fontWeight="medium" mb={1}>
|
||||
热门个股
|
||||
</Text>
|
||||
<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>
|
||||
<HStack spacing={1} flexWrap="wrap">
|
||||
{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>
|
||||
{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>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
<Icon as={ChevronRightIcon} color="purple.500" boxSize={4} />
|
||||
<Icon
|
||||
as={hasFeatureAccess('hot_stocks') ? ChevronRightIcon : FaLock}
|
||||
color={hasFeatureAccess('hot_stocks') ? 'purple.500' : 'yellow.600'}
|
||||
boxSize={4}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
@@ -668,6 +858,173 @@ 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">
|
||||
@@ -768,31 +1125,38 @@ const ConceptCenter = () => {
|
||||
opacity={0.5}
|
||||
/>
|
||||
|
||||
<Container maxW="container.xl" position="relative" py={{ base: 20, md: 32 }}>
|
||||
<VStack spacing={12}>
|
||||
<VStack spacing={6} textAlign="center">
|
||||
<Container maxW="container.xl" position="relative" py={{ base: 12, md: 20 }}>
|
||||
<VStack spacing={8}>
|
||||
<VStack spacing={4} textAlign="center">
|
||||
<HStack spacing={4} justify="center">
|
||||
<Icon as={FaBrain} boxSize={16} color="yellow.300" />
|
||||
</HStack>
|
||||
|
||||
<VStack spacing={3}>
|
||||
<VStack spacing={2}>
|
||||
<Heading
|
||||
as="h1"
|
||||
size="3xl"
|
||||
size="2xl"
|
||||
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>
|
||||
@@ -826,7 +1190,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>
|
||||
|
||||
@@ -865,11 +1229,11 @@ const ConceptCenter = () => {
|
||||
|
||||
<HStack spacing={8} divider={<Divider orientation="vertical" height="40px" borderColor="whiteAlpha.400" />}>
|
||||
<VStack>
|
||||
<Text fontSize="3xl" fontWeight="bold" color="yellow.300">{totalConcepts}</Text>
|
||||
<Text fontSize="3xl" fontWeight="bold" color="yellow.300">500+</Text>
|
||||
<Text fontSize="sm" opacity={0.8}>概念板块</Text>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<Text fontSize="3xl" fontWeight="bold" color="cyan.300">10K+</Text>
|
||||
<Text fontSize="3xl" fontWeight="bold" color="cyan.300">5000+</Text>
|
||||
<Text fontSize="sm" opacity={0.8}>相关个股</Text>
|
||||
</VStack>
|
||||
<VStack>
|
||||
@@ -959,146 +1323,199 @@ const ConceptCenter = () => {
|
||||
<DateSelector />
|
||||
</Box>
|
||||
|
||||
<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"
|
||||
{/* 双栏布局:左侧概念卡片,右侧统计面板 */}
|
||||
<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}
|
||||
>
|
||||
<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>
|
||||
<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, 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>
|
||||
<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>
|
||||
{[...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>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
</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}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
{/* 股票详情Modal */}
|
||||
@@ -1135,6 +1552,16 @@ const ConceptCenter = () => {
|
||||
conceptId={selectedConceptId}
|
||||
/>
|
||||
|
||||
{/* 订阅升级Modal */}
|
||||
<SubscriptionUpgradeModal
|
||||
isOpen={upgradeModalOpen}
|
||||
onClose={() => setUpgradeModalOpen(false)}
|
||||
requiredLevel={upgradeFeature}
|
||||
featureName={
|
||||
upgradeFeature === 'pro' ? '概念统计中心和热门个股' : '概念历史时间轴'
|
||||
}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user