update pay ui
This commit is contained in:
96
app.py
96
app.py
@@ -5508,6 +5508,102 @@ def delete_related_stock(stock_id):
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/events/by-stocks', methods=['POST'])
|
||||
def get_events_by_stocks():
|
||||
"""
|
||||
通过股票代码列表获取关联的事件(新闻)
|
||||
用于概念中心时间轴:聚合概念下所有股票的相关新闻
|
||||
|
||||
请求体:
|
||||
{
|
||||
"stock_codes": ["000001.SZ", "600000.SH", ...], # 股票代码列表
|
||||
"start_date": "2024-01-01", # 可选,开始日期
|
||||
"end_date": "2024-12-31", # 可选,结束日期
|
||||
"limit": 100 # 可选,限制返回数量,默认100
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
stock_codes = data.get('stock_codes', [])
|
||||
start_date_str = data.get('start_date')
|
||||
end_date_str = data.get('end_date')
|
||||
limit = data.get('limit', 100)
|
||||
|
||||
if not stock_codes:
|
||||
return jsonify({'success': False, 'error': '缺少股票代码列表'}), 400
|
||||
|
||||
# 构建查询:通过 RelatedStock 表找到关联的事件
|
||||
query = db.session.query(Event).join(
|
||||
RelatedStock, Event.id == RelatedStock.event_id
|
||||
).filter(
|
||||
RelatedStock.stock_code.in_(stock_codes)
|
||||
)
|
||||
|
||||
# 日期过滤
|
||||
if start_date_str:
|
||||
try:
|
||||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
|
||||
query = query.filter(Event.event_date >= start_date)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date_str:
|
||||
try:
|
||||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
|
||||
# 设置为当天结束
|
||||
end_date = end_date.replace(hour=23, minute=59, second=59)
|
||||
query = query.filter(Event.event_date <= end_date)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 去重并排序
|
||||
query = query.distinct().order_by(Event.event_date.desc())
|
||||
|
||||
# 限制数量
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
events = query.all()
|
||||
|
||||
# 构建返回数据
|
||||
events_data = []
|
||||
for event in events:
|
||||
# 获取该事件关联的股票信息(在请求的股票列表中的)
|
||||
related_stocks_in_list = [
|
||||
{
|
||||
'stock_code': rs.stock_code,
|
||||
'stock_name': rs.stock_name,
|
||||
'sector': rs.sector
|
||||
}
|
||||
for rs in event.related_stocks
|
||||
if rs.stock_code in stock_codes
|
||||
]
|
||||
|
||||
events_data.append({
|
||||
'id': event.id,
|
||||
'title': event.title,
|
||||
'content': event.content,
|
||||
'event_date': event.event_date.isoformat() if event.event_date else None,
|
||||
'published_time': event.event_date.strftime('%Y-%m-%d %H:%M:%S') if event.event_date else None,
|
||||
'source': 'event', # 标记来源为事件系统
|
||||
'importance': event.importance,
|
||||
'view_count': event.view_count,
|
||||
'like_count': event.like_count,
|
||||
'related_stocks': related_stocks_in_list,
|
||||
'cover_image': event.cover_image,
|
||||
'created_at': event.created_at.isoformat() if event.created_at else None
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': events_data,
|
||||
'total': len(events_data)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/events/<int:event_id>/concepts', methods=['GET'])
|
||||
def get_related_concepts(event_id):
|
||||
"""获取相关概念列表"""
|
||||
|
||||
@@ -78,11 +78,17 @@ const REPORT_API_URL = process.env.NODE_ENV === 'production'
|
||||
? '/report-api'
|
||||
: 'http://111.198.58.126:8811';
|
||||
|
||||
// 主应用后端 API 基础 URL(用于获取股票关联的事件/新闻)
|
||||
const MAIN_API_URL = process.env.NODE_ENV === 'production'
|
||||
? ''
|
||||
: 'http://111.198.58.126:5001';
|
||||
|
||||
const ConceptTimelineModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
conceptName,
|
||||
conceptId
|
||||
conceptId,
|
||||
stocks = []
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
|
||||
@@ -334,61 +340,66 @@ const ConceptTimelineModal = ({
|
||||
})
|
||||
);
|
||||
|
||||
// 获取新闻(精确匹配,最近100天,最多100条)
|
||||
// 🔄 添加回退逻辑:如果结果不足30条,去掉 exact_match 参数重新搜索
|
||||
// 获取新闻(通过股票代码聚合关联事件)
|
||||
// 新逻辑:每个概念有关联的股票,通过 related_stock 表聚合所有股票的关联新闻/事件
|
||||
const fetchNews = async () => {
|
||||
try {
|
||||
// 第一次尝试:使用精确匹配
|
||||
const newsParams = new URLSearchParams({
|
||||
query: conceptName,
|
||||
exact_match: 1,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
top_k: 100
|
||||
});
|
||||
// 提取股票代码列表
|
||||
const stockCodes = (stocks || [])
|
||||
.map(s => s.code || s.stock_code)
|
||||
.filter(Boolean);
|
||||
|
||||
const newsUrl = `${NEWS_API_URL}/search_china_news?${newsParams}`;
|
||||
const res = await fetch(newsUrl);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logger.error('ConceptTimelineModal', 'fetchTimelineData - News API (exact_match=1)', new Error(`HTTP ${res.status}`), { conceptName, status: res.status, response: text.substring(0, 200) });
|
||||
if (stockCodes.length === 0) {
|
||||
logger.warn('ConceptTimelineModal', '概念没有关联股票,无法获取新闻', { conceptName });
|
||||
return [];
|
||||
}
|
||||
|
||||
const newsResult = await res.json();
|
||||
const newsArray = Array.isArray(newsResult) ? newsResult : [];
|
||||
logger.info('ConceptTimelineModal', `通过 ${stockCodes.length} 只股票获取关联新闻`, { conceptName, stockCodes: stockCodes.slice(0, 5) });
|
||||
|
||||
// 检查结果数量,如果不足30条则进行回退搜索
|
||||
if (newsArray.length < 30) {
|
||||
logger.info('ConceptTimelineModal', `新闻精确搜索结果不足30条 (${newsArray.length}),尝试模糊搜索`, { conceptName });
|
||||
|
||||
// 第二次尝试:去掉精确匹配参数
|
||||
const fallbackParams = new URLSearchParams({
|
||||
query: conceptName,
|
||||
// 调用后端新 API 获取股票关联的事件
|
||||
const res = await fetch(`${MAIN_API_URL}/api/events/by-stocks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stock_codes: stockCodes,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
top_k: 100
|
||||
});
|
||||
limit: 200
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const fallbackUrl = `${NEWS_API_URL}/search_china_news?${fallbackParams}`;
|
||||
const fallbackRes = await fetch(fallbackUrl);
|
||||
|
||||
if (!fallbackRes.ok) {
|
||||
logger.warn('ConceptTimelineModal', '新闻模糊搜索失败,使用精确搜索结果', { conceptName });
|
||||
return newsArray;
|
||||
}
|
||||
|
||||
const fallbackResult = await fallbackRes.json();
|
||||
const fallbackArray = Array.isArray(fallbackResult) ? fallbackResult : [];
|
||||
|
||||
logger.info('ConceptTimelineModal', `新闻模糊搜索成功,获取 ${fallbackArray.length} 条结果`, { conceptName });
|
||||
return fallbackArray;
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', new Error(`HTTP ${res.status}`), { conceptName, status: res.status, response: text.substring(0, 200) });
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn('ConceptTimelineModal', '获取股票关联事件失败', { conceptName, error: result.error });
|
||||
return [];
|
||||
}
|
||||
|
||||
// 转换为新闻格式
|
||||
const events = result.data || [];
|
||||
const newsArray = events.map(event => ({
|
||||
title: event.title,
|
||||
detail: event.description || event.summary || '',
|
||||
description: event.description || event.summary || '',
|
||||
published_time: event.event_date || event.created_at,
|
||||
source: 'event', // 标记为事件来源
|
||||
url: null, // 事件没有外链
|
||||
related_stocks: event.related_stocks || [] // 保留关联股票信息
|
||||
}));
|
||||
|
||||
logger.info('ConceptTimelineModal', `获取到 ${newsArray.length} 条股票关联事件`, { conceptName });
|
||||
return newsArray;
|
||||
} catch (err) {
|
||||
logger.error('ConceptTimelineModal', 'fetchTimelineData - News API', err, { conceptName });
|
||||
logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', err, { conceptName });
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -1376,10 +1387,10 @@ const ConceptTimelineModal = ({
|
||||
<HStack spacing={3} fontSize="sm">
|
||||
{selectedNews?.source && (
|
||||
<Badge
|
||||
bg={selectedNews.source === 'zsxq' ? 'purple.500' : 'blue.500'}
|
||||
bg={selectedNews.source === 'zsxq' ? 'purple.500' : selectedNews.source === 'event' ? 'cyan.500' : 'blue.500'}
|
||||
color="white"
|
||||
>
|
||||
{selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source}
|
||||
{selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source === 'event' ? '关联事件' : selectedNews.source}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedNews?.time && (
|
||||
@@ -1411,8 +1422,8 @@ const ConceptTimelineModal = ({
|
||||
|
||||
<ModalFooter borderTop="1px solid" borderColor="whiteAlpha.100" bg="rgba(15, 23, 42, 0.98)">
|
||||
<HStack spacing={3}>
|
||||
{/* zsxq来源不显示查看原文按钮 */}
|
||||
{selectedNews?.url && selectedNews?.source !== 'zsxq' && (
|
||||
{/* zsxq和event来源不显示查看原文按钮 */}
|
||||
{selectedNews?.url && selectedNews?.source !== 'zsxq' && selectedNews?.source !== 'event' && (
|
||||
<Button
|
||||
size="sm"
|
||||
bg="whiteAlpha.100"
|
||||
|
||||
@@ -71,27 +71,27 @@ const glowPulse = keyframes`
|
||||
50% { opacity: 0.6; transform: scale(1.05); }
|
||||
`;
|
||||
|
||||
// 一级分类颜色映射(基础色 - 半透明玻璃态 - 黑金主题)
|
||||
// 一级分类颜色映射(基础色 - 半透明玻璃态)
|
||||
const LV1_COLORS = {
|
||||
'人工智能': '#EAB308', // 金色
|
||||
'半导体': '#F59E0B', // 琥珀色
|
||||
'机器人': '#CA8A04', // 深金色
|
||||
'消费电子': '#FACC15', // 亮金色
|
||||
'智能驾驶与汽车': '#D97706', // 橙金色
|
||||
'新能源与电力': '#FDE047', // 浅金色
|
||||
'空天经济': '#A16207', // 古铜金
|
||||
'国防军工': '#B45309', // 铜色
|
||||
'政策与主题': '#EAB308', // 金色
|
||||
'周期与材料': '#F59E0B', // 琥珀色
|
||||
'大消费': '#FACC15', // 亮金色
|
||||
'数字经济与金融科技': '#CA8A04', // 深金色
|
||||
'全球宏观与贸易': '#D97706', // 橙金色
|
||||
'医药健康': '#FDE047', // 浅金色
|
||||
'前沿科技': '#EAB308', // 金色
|
||||
'人工智能': '#8B5CF6',
|
||||
'半导体': '#3B82F6',
|
||||
'机器人': '#10B981',
|
||||
'消费电子': '#F59E0B',
|
||||
'智能驾驶与汽车': '#EF4444',
|
||||
'新能源与电力': '#06B6D4',
|
||||
'空天经济': '#6366F1',
|
||||
'国防军工': '#EC4899',
|
||||
'政策与主题': '#14B8A6',
|
||||
'周期与材料': '#F97316',
|
||||
'大消费': '#A855F7',
|
||||
'数字经济与金融科技': '#22D3EE',
|
||||
'全球宏观与贸易': '#84CC16',
|
||||
'医药健康': '#E879F9',
|
||||
'前沿科技': '#38BDF8',
|
||||
};
|
||||
|
||||
// 根据涨跌幅获取颜色(涨红跌绿 - 玻璃态半透明)
|
||||
const getChangeColor = (value, baseColor = '#A16207') => {
|
||||
const getChangeColor = (value, baseColor = '#64748B') => {
|
||||
if (value === null || value === undefined) return baseColor;
|
||||
|
||||
// 涨 - 红色系(调整透明度使其更柔和)
|
||||
@@ -681,7 +681,7 @@ const ForceGraphView = ({
|
||||
gapWidth: 4,
|
||||
borderRadius: 16,
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(234, 179, 8, 0.3)',
|
||||
shadowColor: 'rgba(139, 92, 246, 0.3)',
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
@@ -717,7 +717,7 @@ const ForceGraphView = ({
|
||||
gapWidth: 3,
|
||||
borderRadius: 12,
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(234, 179, 8, 0.2)',
|
||||
shadowColor: 'rgba(139, 92, 246, 0.2)',
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
@@ -810,12 +810,12 @@ const ForceGraphView = ({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(12, 10, 9, 0.95)',
|
||||
borderColor: 'rgba(234, 179, 8, 0.4)',
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.9)',
|
||||
borderColor: 'rgba(139, 92, 246, 0.4)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 16,
|
||||
padding: [14, 18],
|
||||
extraCssText: 'backdrop-filter: blur(20px); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);',
|
||||
extraCssText: 'backdrop-filter: blur(20px); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);',
|
||||
textStyle: {
|
||||
color: '#E2E8F0',
|
||||
fontSize: 13,
|
||||
@@ -871,11 +871,11 @@ const ForceGraphView = ({
|
||||
${data.conceptCount ? `<span>📁 ${data.conceptCount} 个概念</span>` : ''}
|
||||
</div>
|
||||
${data.level === 'concept' ? `
|
||||
<div style="color: #FACC15; font-size: 11px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<div style="color: #A78BFA; font-size: 11px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
🔗 点击查看概念详情
|
||||
</div>
|
||||
` : data.level !== 'concept' ? `
|
||||
<div style="color: #A1A1AA; font-size: 11px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<div style="color: #64748B; font-size: 11px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
📂 点击进入查看子分类
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -911,7 +911,7 @@ const ForceGraphView = ({
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 30,
|
||||
shadowColor: 'rgba(234, 179, 8, 0.5)',
|
||||
shadowColor: 'rgba(139, 92, 246, 0.5)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
},
|
||||
@@ -1014,7 +1014,7 @@ const ForceGraphView = ({
|
||||
return (
|
||||
<Center
|
||||
h="500px"
|
||||
bg="rgba(12, 10, 9, 0.6)"
|
||||
bg="rgba(15, 23, 42, 0.6)"
|
||||
borderRadius="3xl"
|
||||
backdropFilter="blur(20px)"
|
||||
border="1px solid"
|
||||
@@ -1027,13 +1027,13 @@ const ForceGraphView = ({
|
||||
<Box
|
||||
position="absolute"
|
||||
inset={-4}
|
||||
bg="yellow.500"
|
||||
bg="purple.500"
|
||||
borderRadius="full"
|
||||
filter="blur(20px)"
|
||||
opacity={0.4}
|
||||
animation={`${glowPulse} 2s ease-in-out infinite`}
|
||||
/>
|
||||
<Spinner size="xl" color="yellow.400" thickness="4px" />
|
||||
<Spinner size="xl" color="purple.400" thickness="4px" />
|
||||
</Box>
|
||||
<Text color="gray.400" fontSize="sm">正在构建矩形树图...</Text>
|
||||
</VStack>
|
||||
@@ -1045,7 +1045,7 @@ const ForceGraphView = ({
|
||||
return (
|
||||
<Center
|
||||
h="500px"
|
||||
bg="rgba(12, 10, 9, 0.6)"
|
||||
bg="rgba(15, 23, 42, 0.6)"
|
||||
borderRadius="3xl"
|
||||
backdropFilter="blur(20px)"
|
||||
border="1px solid"
|
||||
@@ -1055,7 +1055,7 @@ const ForceGraphView = ({
|
||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.600" />
|
||||
<Text color="gray.400">加载失败:{error}</Text>
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
colorScheme="purple"
|
||||
size="sm"
|
||||
onClick={fetchHierarchy}
|
||||
borderRadius="full"
|
||||
@@ -1084,26 +1084,26 @@ const ForceGraphView = ({
|
||||
h={containerHeight}
|
||||
bg="transparent"
|
||||
>
|
||||
{/* 极光背景层 - 黑金主题 */}
|
||||
{/* 极光背景层 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="linear-gradient(135deg, #0C0A09 0%, #1C1917 25%, #292524 50%, #1C1917 75%, #0C0A09 100%)"
|
||||
bg="linear-gradient(135deg, #0F172A 0%, #1E1B4B 25%, #312E81 50%, #1E1B4B 75%, #0F172A 100%)"
|
||||
backgroundSize="400% 400%"
|
||||
animation={`${auroraAnimation} 15s ease infinite`}
|
||||
/>
|
||||
|
||||
{/* 弥散光晕层 - 黑金主题 */}
|
||||
{/* 弥散光晕层 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="10%"
|
||||
w="300px"
|
||||
h="300px"
|
||||
bg="radial-gradient(circle, rgba(234, 179, 8, 0.25) 0%, transparent 70%)"
|
||||
bg="radial-gradient(circle, rgba(139, 92, 246, 0.3) 0%, transparent 70%)"
|
||||
filter="blur(60px)"
|
||||
pointerEvents="none"
|
||||
animation={`${glowPulse} 4s ease-in-out infinite`}
|
||||
@@ -1114,7 +1114,7 @@ const ForceGraphView = ({
|
||||
right="15%"
|
||||
w="250px"
|
||||
h="250px"
|
||||
bg="radial-gradient(circle, rgba(245, 158, 11, 0.2) 0%, transparent 70%)"
|
||||
bg="radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)"
|
||||
filter="blur(50px)"
|
||||
pointerEvents="none"
|
||||
animation={`${glowPulse} 5s ease-in-out infinite 1s`}
|
||||
@@ -1125,7 +1125,7 @@ const ForceGraphView = ({
|
||||
right="30%"
|
||||
w="200px"
|
||||
h="200px"
|
||||
bg="radial-gradient(circle, rgba(217, 119, 6, 0.15) 0%, transparent 70%)"
|
||||
bg="radial-gradient(circle, rgba(236, 72, 153, 0.2) 0%, transparent 70%)"
|
||||
filter="blur(40px)"
|
||||
pointerEvents="none"
|
||||
animation={`${glowPulse} 6s ease-in-out infinite 2s`}
|
||||
@@ -1159,8 +1159,8 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
borderRadius="full"
|
||||
_hover={{
|
||||
bg: 'rgba(234, 179, 8, 0.4)',
|
||||
borderColor: 'yellow.400',
|
||||
bg: 'rgba(139, 92, 246, 0.4)',
|
||||
borderColor: 'purple.400',
|
||||
transform: 'scale(1.05)',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
@@ -1179,7 +1179,7 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.150"
|
||||
boxShadow="0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)"
|
||||
>
|
||||
<Icon as={FaTh} color="yellow.400" />
|
||||
<Icon as={FaTh} color="purple.300" />
|
||||
<Text color="white" fontWeight="bold" fontSize="sm">
|
||||
概念矩形树图
|
||||
</Text>
|
||||
@@ -1202,7 +1202,7 @@ const ForceGraphView = ({
|
||||
<Icon as={FaChevronRight} color="whiteAlpha.400" boxSize={3} />
|
||||
)}
|
||||
<Text
|
||||
color={index === breadcrumbItems.length - 1 ? 'yellow.400' : 'whiteAlpha.700'}
|
||||
color={index === breadcrumbItems.length - 1 ? 'purple.300' : 'whiteAlpha.700'}
|
||||
fontSize="sm"
|
||||
fontWeight={index === breadcrumbItems.length - 1 ? 'bold' : 'normal'}
|
||||
cursor={index < breadcrumbItems.length - 1 ? 'pointer' : 'default'}
|
||||
@@ -1224,7 +1224,7 @@ const ForceGraphView = ({
|
||||
|
||||
{/* 右侧控制按钮 */}
|
||||
<HStack spacing={2} pointerEvents="auto">
|
||||
{priceLoading && <Spinner size="sm" color="yellow.400" />}
|
||||
{priceLoading && <Spinner size="sm" color="purple.300" />}
|
||||
|
||||
{drillPath && (
|
||||
<Tooltip label="返回全部" placement="left">
|
||||
@@ -1239,8 +1239,8 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
borderRadius="full"
|
||||
_hover={{
|
||||
bg: 'rgba(234, 179, 8, 0.4)',
|
||||
borderColor: 'yellow.400',
|
||||
bg: 'rgba(139, 92, 246, 0.4)',
|
||||
borderColor: 'purple.400',
|
||||
transform: 'scale(1.05)',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
|
||||
@@ -342,6 +342,7 @@ const ConceptCenter = () => {
|
||||
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);
|
||||
@@ -367,7 +368,7 @@ const ConceptCenter = () => {
|
||||
return null;
|
||||
}, []);
|
||||
// 打开内容模态框(新闻和研报)- 需要Max版权限
|
||||
const handleViewContent = (e, conceptName, conceptId) => {
|
||||
const handleViewContent = (e, conceptName, conceptId, stocks = []) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// 检查历史时间轴权限
|
||||
@@ -383,6 +384,7 @@ const ConceptCenter = () => {
|
||||
|
||||
setSelectedConceptForContent(conceptName);
|
||||
setSelectedConceptId(conceptId);
|
||||
setSelectedConceptStocksForTimeline(stocks || []);
|
||||
setIsTimelineModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -1110,7 +1112,7 @@ const ConceptCenter = () => {
|
||||
bg="purple.500"
|
||||
color="white"
|
||||
variant="solid"
|
||||
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id)}
|
||||
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id, concept.stocks)}
|
||||
borderRadius="full"
|
||||
px={{ base: 2, md: 4 }}
|
||||
fontWeight="medium"
|
||||
@@ -1296,7 +1298,7 @@ const ConceptCenter = () => {
|
||||
leftIcon={<FaChartLine />}
|
||||
bg="purple.500"
|
||||
color="white"
|
||||
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id)}
|
||||
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)' }}
|
||||
@@ -1769,12 +1771,12 @@ const ConceptCenter = () => {
|
||||
setViewMode('force3d');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'force3d' ? 'yellow.500' : 'transparent'}
|
||||
color={viewMode === 'force3d' ? 'gray.900' : 'whiteAlpha.700'}
|
||||
bg={viewMode === 'force3d' ? 'purple.500' : 'transparent'}
|
||||
color={viewMode === 'force3d' ? 'white' : 'whiteAlpha.700'}
|
||||
borderColor="whiteAlpha.300"
|
||||
_hover={{
|
||||
bg: viewMode === 'force3d' ? 'yellow.400' : 'whiteAlpha.100',
|
||||
boxShadow: viewMode === 'force3d' ? '0 0 10px rgba(234, 179, 8, 0.4)' : 'none',
|
||||
bg: viewMode === 'force3d' ? 'purple.400' : 'whiteAlpha.100',
|
||||
boxShadow: viewMode === 'force3d' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
|
||||
}}
|
||||
aria-label="概念矩形树图"
|
||||
/>
|
||||
@@ -1788,12 +1790,12 @@ const ConceptCenter = () => {
|
||||
setViewMode('hierarchy');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'hierarchy' ? 'yellow.500' : 'transparent'}
|
||||
color={viewMode === 'hierarchy' ? 'gray.900' : 'whiteAlpha.700'}
|
||||
bg={viewMode === 'hierarchy' ? 'purple.500' : 'transparent'}
|
||||
color={viewMode === 'hierarchy' ? 'white' : 'whiteAlpha.700'}
|
||||
borderColor="whiteAlpha.300"
|
||||
_hover={{
|
||||
bg: viewMode === 'hierarchy' ? 'yellow.400' : 'whiteAlpha.100',
|
||||
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(234, 179, 8, 0.4)' : 'none',
|
||||
bg: viewMode === 'hierarchy' ? 'purple.400' : 'whiteAlpha.100',
|
||||
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
|
||||
}}
|
||||
aria-label="层级图"
|
||||
/>
|
||||
@@ -1807,12 +1809,12 @@ const ConceptCenter = () => {
|
||||
setViewMode('list');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'list' ? 'yellow.500' : 'transparent'}
|
||||
color={viewMode === 'list' ? 'gray.900' : 'whiteAlpha.700'}
|
||||
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
|
||||
color={viewMode === 'list' ? 'white' : 'whiteAlpha.700'}
|
||||
borderColor="whiteAlpha.300"
|
||||
_hover={{
|
||||
bg: viewMode === 'list' ? 'yellow.400' : 'whiteAlpha.100',
|
||||
boxShadow: viewMode === 'list' ? '0 0 10px rgba(234, 179, 8, 0.4)' : 'none',
|
||||
bg: viewMode === 'list' ? 'purple.400' : 'whiteAlpha.100',
|
||||
boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
|
||||
}}
|
||||
aria-label="列表视图"
|
||||
/>
|
||||
@@ -2057,6 +2059,7 @@ const ConceptCenter = () => {
|
||||
onClose={() => setIsTimelineModalOpen(false)}
|
||||
conceptName={selectedConceptForContent}
|
||||
conceptId={selectedConceptId}
|
||||
stocks={selectedConceptStocksForTimeline}
|
||||
/>
|
||||
|
||||
{/* 订阅升级Modal */}
|
||||
|
||||
Reference in New Issue
Block a user