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
|
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'])
|
@app.route('/api/events/<int:event_id>/concepts', methods=['GET'])
|
||||||
def get_related_concepts(event_id):
|
def get_related_concepts(event_id):
|
||||||
"""获取相关概念列表"""
|
"""获取相关概念列表"""
|
||||||
|
|||||||
@@ -78,11 +78,17 @@ const REPORT_API_URL = process.env.NODE_ENV === 'production'
|
|||||||
? '/report-api'
|
? '/report-api'
|
||||||
: 'http://111.198.58.126:8811';
|
: '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 = ({
|
const ConceptTimelineModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
conceptName,
|
conceptName,
|
||||||
conceptId
|
conceptId,
|
||||||
|
stocks = []
|
||||||
}) => {
|
}) => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -334,61 +340,66 @@ const ConceptTimelineModal = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取新闻(精确匹配,最近100天,最多100条)
|
// 获取新闻(通过股票代码聚合关联事件)
|
||||||
// 🔄 添加回退逻辑:如果结果不足30条,去掉 exact_match 参数重新搜索
|
// 新逻辑:每个概念有关联的股票,通过 related_stock 表聚合所有股票的关联新闻/事件
|
||||||
const fetchNews = async () => {
|
const fetchNews = async () => {
|
||||||
try {
|
try {
|
||||||
// 第一次尝试:使用精确匹配
|
// 提取股票代码列表
|
||||||
const newsParams = new URLSearchParams({
|
const stockCodes = (stocks || [])
|
||||||
query: conceptName,
|
.map(s => s.code || s.stock_code)
|
||||||
exact_match: 1,
|
.filter(Boolean);
|
||||||
start_date: startDateStr,
|
|
||||||
end_date: endDateStr,
|
|
||||||
top_k: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
const newsUrl = `${NEWS_API_URL}/search_china_news?${newsParams}`;
|
if (stockCodes.length === 0) {
|
||||||
const res = await fetch(newsUrl);
|
logger.warn('ConceptTimelineModal', '概念没有关联股票,无法获取新闻', { conceptName });
|
||||||
|
|
||||||
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) });
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const newsResult = await res.json();
|
logger.info('ConceptTimelineModal', `通过 ${stockCodes.length} 只股票获取关联新闻`, { conceptName, stockCodes: stockCodes.slice(0, 5) });
|
||||||
const newsArray = Array.isArray(newsResult) ? newsResult : [];
|
|
||||||
|
|
||||||
// 检查结果数量,如果不足30条则进行回退搜索
|
// 调用后端新 API 获取股票关联的事件
|
||||||
if (newsArray.length < 30) {
|
const res = await fetch(`${MAIN_API_URL}/api/events/by-stocks`, {
|
||||||
logger.info('ConceptTimelineModal', `新闻精确搜索结果不足30条 (${newsArray.length}),尝试模糊搜索`, { conceptName });
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
// 第二次尝试:去掉精确匹配参数
|
'Content-Type': 'application/json',
|
||||||
const fallbackParams = new URLSearchParams({
|
},
|
||||||
query: conceptName,
|
body: JSON.stringify({
|
||||||
|
stock_codes: stockCodes,
|
||||||
start_date: startDateStr,
|
start_date: startDateStr,
|
||||||
end_date: endDateStr,
|
end_date: endDateStr,
|
||||||
top_k: 100
|
limit: 200
|
||||||
});
|
}),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
const fallbackUrl = `${NEWS_API_URL}/search_china_news?${fallbackParams}`;
|
if (!res.ok) {
|
||||||
const fallbackRes = await fetch(fallbackUrl);
|
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) });
|
||||||
if (!fallbackRes.ok) {
|
return [];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return newsArray;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('ConceptTimelineModal', 'fetchTimelineData - News API', err, { conceptName });
|
logger.error('ConceptTimelineModal', 'fetchTimelineData - Events by Stocks API', err, { conceptName });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1376,10 +1387,10 @@ const ConceptTimelineModal = ({
|
|||||||
<HStack spacing={3} fontSize="sm">
|
<HStack spacing={3} fontSize="sm">
|
||||||
{selectedNews?.source && (
|
{selectedNews?.source && (
|
||||||
<Badge
|
<Badge
|
||||||
bg={selectedNews.source === 'zsxq' ? 'purple.500' : 'blue.500'}
|
bg={selectedNews.source === 'zsxq' ? 'purple.500' : selectedNews.source === 'event' ? 'cyan.500' : 'blue.500'}
|
||||||
color="white"
|
color="white"
|
||||||
>
|
>
|
||||||
{selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source}
|
{selectedNews.source === 'zsxq' ? '知识星球' : selectedNews.source === 'event' ? '关联事件' : selectedNews.source}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{selectedNews?.time && (
|
{selectedNews?.time && (
|
||||||
@@ -1411,8 +1422,8 @@ const ConceptTimelineModal = ({
|
|||||||
|
|
||||||
<ModalFooter borderTop="1px solid" borderColor="whiteAlpha.100" bg="rgba(15, 23, 42, 0.98)">
|
<ModalFooter borderTop="1px solid" borderColor="whiteAlpha.100" bg="rgba(15, 23, 42, 0.98)">
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
{/* zsxq来源不显示查看原文按钮 */}
|
{/* zsxq和event来源不显示查看原文按钮 */}
|
||||||
{selectedNews?.url && selectedNews?.source !== 'zsxq' && (
|
{selectedNews?.url && selectedNews?.source !== 'zsxq' && selectedNews?.source !== 'event' && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
bg="whiteAlpha.100"
|
bg="whiteAlpha.100"
|
||||||
|
|||||||
@@ -71,27 +71,27 @@ const glowPulse = keyframes`
|
|||||||
50% { opacity: 0.6; transform: scale(1.05); }
|
50% { opacity: 0.6; transform: scale(1.05); }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 一级分类颜色映射(基础色 - 半透明玻璃态 - 黑金主题)
|
// 一级分类颜色映射(基础色 - 半透明玻璃态)
|
||||||
const LV1_COLORS = {
|
const LV1_COLORS = {
|
||||||
'人工智能': '#EAB308', // 金色
|
'人工智能': '#8B5CF6',
|
||||||
'半导体': '#F59E0B', // 琥珀色
|
'半导体': '#3B82F6',
|
||||||
'机器人': '#CA8A04', // 深金色
|
'机器人': '#10B981',
|
||||||
'消费电子': '#FACC15', // 亮金色
|
'消费电子': '#F59E0B',
|
||||||
'智能驾驶与汽车': '#D97706', // 橙金色
|
'智能驾驶与汽车': '#EF4444',
|
||||||
'新能源与电力': '#FDE047', // 浅金色
|
'新能源与电力': '#06B6D4',
|
||||||
'空天经济': '#A16207', // 古铜金
|
'空天经济': '#6366F1',
|
||||||
'国防军工': '#B45309', // 铜色
|
'国防军工': '#EC4899',
|
||||||
'政策与主题': '#EAB308', // 金色
|
'政策与主题': '#14B8A6',
|
||||||
'周期与材料': '#F59E0B', // 琥珀色
|
'周期与材料': '#F97316',
|
||||||
'大消费': '#FACC15', // 亮金色
|
'大消费': '#A855F7',
|
||||||
'数字经济与金融科技': '#CA8A04', // 深金色
|
'数字经济与金融科技': '#22D3EE',
|
||||||
'全球宏观与贸易': '#D97706', // 橙金色
|
'全球宏观与贸易': '#84CC16',
|
||||||
'医药健康': '#FDE047', // 浅金色
|
'医药健康': '#E879F9',
|
||||||
'前沿科技': '#EAB308', // 金色
|
'前沿科技': '#38BDF8',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据涨跌幅获取颜色(涨红跌绿 - 玻璃态半透明)
|
// 根据涨跌幅获取颜色(涨红跌绿 - 玻璃态半透明)
|
||||||
const getChangeColor = (value, baseColor = '#A16207') => {
|
const getChangeColor = (value, baseColor = '#64748B') => {
|
||||||
if (value === null || value === undefined) return baseColor;
|
if (value === null || value === undefined) return baseColor;
|
||||||
|
|
||||||
// 涨 - 红色系(调整透明度使其更柔和)
|
// 涨 - 红色系(调整透明度使其更柔和)
|
||||||
@@ -681,7 +681,7 @@ const ForceGraphView = ({
|
|||||||
gapWidth: 4,
|
gapWidth: 4,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
shadowBlur: 20,
|
shadowBlur: 20,
|
||||||
shadowColor: 'rgba(234, 179, 8, 0.3)',
|
shadowColor: 'rgba(139, 92, 246, 0.3)',
|
||||||
},
|
},
|
||||||
upperLabel: {
|
upperLabel: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -717,7 +717,7 @@ const ForceGraphView = ({
|
|||||||
gapWidth: 3,
|
gapWidth: 3,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
shadowBlur: 10,
|
shadowBlur: 10,
|
||||||
shadowColor: 'rgba(234, 179, 8, 0.2)',
|
shadowColor: 'rgba(139, 92, 246, 0.2)',
|
||||||
},
|
},
|
||||||
upperLabel: {
|
upperLabel: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -810,12 +810,12 @@ const ForceGraphView = ({
|
|||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
backgroundColor: 'rgba(12, 10, 9, 0.95)',
|
backgroundColor: 'rgba(15, 23, 42, 0.9)',
|
||||||
borderColor: 'rgba(234, 179, 8, 0.4)',
|
borderColor: 'rgba(139, 92, 246, 0.4)',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: [14, 18],
|
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: {
|
textStyle: {
|
||||||
color: '#E2E8F0',
|
color: '#E2E8F0',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -871,11 +871,11 @@ const ForceGraphView = ({
|
|||||||
${data.conceptCount ? `<span>📁 ${data.conceptCount} 个概念</span>` : ''}
|
${data.conceptCount ? `<span>📁 ${data.conceptCount} 个概念</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${data.level === 'concept' ? `
|
${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>
|
</div>
|
||||||
` : data.level !== 'concept' ? `
|
` : 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>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -911,7 +911,7 @@ const ForceGraphView = ({
|
|||||||
emphasis: {
|
emphasis: {
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
shadowBlur: 30,
|
shadowBlur: 30,
|
||||||
shadowColor: 'rgba(234, 179, 8, 0.5)',
|
shadowColor: 'rgba(139, 92, 246, 0.5)',
|
||||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1014,7 +1014,7 @@ const ForceGraphView = ({
|
|||||||
return (
|
return (
|
||||||
<Center
|
<Center
|
||||||
h="500px"
|
h="500px"
|
||||||
bg="rgba(12, 10, 9, 0.6)"
|
bg="rgba(15, 23, 42, 0.6)"
|
||||||
borderRadius="3xl"
|
borderRadius="3xl"
|
||||||
backdropFilter="blur(20px)"
|
backdropFilter="blur(20px)"
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
@@ -1027,13 +1027,13 @@ const ForceGraphView = ({
|
|||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
inset={-4}
|
inset={-4}
|
||||||
bg="yellow.500"
|
bg="purple.500"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
filter="blur(20px)"
|
filter="blur(20px)"
|
||||||
opacity={0.4}
|
opacity={0.4}
|
||||||
animation={`${glowPulse} 2s ease-in-out infinite`}
|
animation={`${glowPulse} 2s ease-in-out infinite`}
|
||||||
/>
|
/>
|
||||||
<Spinner size="xl" color="yellow.400" thickness="4px" />
|
<Spinner size="xl" color="purple.400" thickness="4px" />
|
||||||
</Box>
|
</Box>
|
||||||
<Text color="gray.400" fontSize="sm">正在构建矩形树图...</Text>
|
<Text color="gray.400" fontSize="sm">正在构建矩形树图...</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -1045,7 +1045,7 @@ const ForceGraphView = ({
|
|||||||
return (
|
return (
|
||||||
<Center
|
<Center
|
||||||
h="500px"
|
h="500px"
|
||||||
bg="rgba(12, 10, 9, 0.6)"
|
bg="rgba(15, 23, 42, 0.6)"
|
||||||
borderRadius="3xl"
|
borderRadius="3xl"
|
||||||
backdropFilter="blur(20px)"
|
backdropFilter="blur(20px)"
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
@@ -1055,7 +1055,7 @@ const ForceGraphView = ({
|
|||||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.600" />
|
<Icon as={FaLayerGroup} boxSize={16} color="gray.600" />
|
||||||
<Text color="gray.400">加载失败:{error}</Text>
|
<Text color="gray.400">加载失败:{error}</Text>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="yellow"
|
colorScheme="purple"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={fetchHierarchy}
|
onClick={fetchHierarchy}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
@@ -1084,26 +1084,26 @@ const ForceGraphView = ({
|
|||||||
h={containerHeight}
|
h={containerHeight}
|
||||||
bg="transparent"
|
bg="transparent"
|
||||||
>
|
>
|
||||||
{/* 极光背景层 - 黑金主题 */}
|
{/* 极光背景层 */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={0}
|
top={0}
|
||||||
left={0}
|
left={0}
|
||||||
right={0}
|
right={0}
|
||||||
bottom={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%"
|
backgroundSize="400% 400%"
|
||||||
animation={`${auroraAnimation} 15s ease infinite`}
|
animation={`${auroraAnimation} 15s ease infinite`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 弥散光晕层 - 黑金主题 */}
|
{/* 弥散光晕层 */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top="20%"
|
top="20%"
|
||||||
left="10%"
|
left="10%"
|
||||||
w="300px"
|
w="300px"
|
||||||
h="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)"
|
filter="blur(60px)"
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
animation={`${glowPulse} 4s ease-in-out infinite`}
|
animation={`${glowPulse} 4s ease-in-out infinite`}
|
||||||
@@ -1114,7 +1114,7 @@ const ForceGraphView = ({
|
|||||||
right="15%"
|
right="15%"
|
||||||
w="250px"
|
w="250px"
|
||||||
h="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)"
|
filter="blur(50px)"
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
animation={`${glowPulse} 5s ease-in-out infinite 1s`}
|
animation={`${glowPulse} 5s ease-in-out infinite 1s`}
|
||||||
@@ -1125,7 +1125,7 @@ const ForceGraphView = ({
|
|||||||
right="30%"
|
right="30%"
|
||||||
w="200px"
|
w="200px"
|
||||||
h="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)"
|
filter="blur(40px)"
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
animation={`${glowPulse} 6s ease-in-out infinite 2s`}
|
animation={`${glowPulse} 6s ease-in-out infinite 2s`}
|
||||||
@@ -1159,8 +1159,8 @@ const ForceGraphView = ({
|
|||||||
borderColor="whiteAlpha.200"
|
borderColor="whiteAlpha.200"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
_hover={{
|
_hover={{
|
||||||
bg: 'rgba(234, 179, 8, 0.4)',
|
bg: 'rgba(139, 92, 246, 0.4)',
|
||||||
borderColor: 'yellow.400',
|
borderColor: 'purple.400',
|
||||||
transform: 'scale(1.05)',
|
transform: 'scale(1.05)',
|
||||||
}}
|
}}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
@@ -1179,7 +1179,7 @@ const ForceGraphView = ({
|
|||||||
borderColor="whiteAlpha.150"
|
borderColor="whiteAlpha.150"
|
||||||
boxShadow="0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)"
|
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 color="white" fontWeight="bold" fontSize="sm">
|
||||||
概念矩形树图
|
概念矩形树图
|
||||||
</Text>
|
</Text>
|
||||||
@@ -1202,7 +1202,7 @@ const ForceGraphView = ({
|
|||||||
<Icon as={FaChevronRight} color="whiteAlpha.400" boxSize={3} />
|
<Icon as={FaChevronRight} color="whiteAlpha.400" boxSize={3} />
|
||||||
)}
|
)}
|
||||||
<Text
|
<Text
|
||||||
color={index === breadcrumbItems.length - 1 ? 'yellow.400' : 'whiteAlpha.700'}
|
color={index === breadcrumbItems.length - 1 ? 'purple.300' : 'whiteAlpha.700'}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
fontWeight={index === breadcrumbItems.length - 1 ? 'bold' : 'normal'}
|
fontWeight={index === breadcrumbItems.length - 1 ? 'bold' : 'normal'}
|
||||||
cursor={index < breadcrumbItems.length - 1 ? 'pointer' : 'default'}
|
cursor={index < breadcrumbItems.length - 1 ? 'pointer' : 'default'}
|
||||||
@@ -1224,7 +1224,7 @@ const ForceGraphView = ({
|
|||||||
|
|
||||||
{/* 右侧控制按钮 */}
|
{/* 右侧控制按钮 */}
|
||||||
<HStack spacing={2} pointerEvents="auto">
|
<HStack spacing={2} pointerEvents="auto">
|
||||||
{priceLoading && <Spinner size="sm" color="yellow.400" />}
|
{priceLoading && <Spinner size="sm" color="purple.300" />}
|
||||||
|
|
||||||
{drillPath && (
|
{drillPath && (
|
||||||
<Tooltip label="返回全部" placement="left">
|
<Tooltip label="返回全部" placement="left">
|
||||||
@@ -1239,8 +1239,8 @@ const ForceGraphView = ({
|
|||||||
borderColor="whiteAlpha.200"
|
borderColor="whiteAlpha.200"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
_hover={{
|
_hover={{
|
||||||
bg: 'rgba(234, 179, 8, 0.4)',
|
bg: 'rgba(139, 92, 246, 0.4)',
|
||||||
borderColor: 'yellow.400',
|
borderColor: 'purple.400',
|
||||||
transform: 'scale(1.05)',
|
transform: 'scale(1.05)',
|
||||||
}}
|
}}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ const ConceptCenter = () => {
|
|||||||
const [selectedConceptName, setSelectedConceptName] = useState('');
|
const [selectedConceptName, setSelectedConceptName] = useState('');
|
||||||
const [isTimelineModalOpen, setIsTimelineModalOpen] = useState(false);
|
const [isTimelineModalOpen, setIsTimelineModalOpen] = useState(false);
|
||||||
const [selectedConceptId, setSelectedConceptId] = useState('');
|
const [selectedConceptId, setSelectedConceptId] = useState('');
|
||||||
|
const [selectedConceptStocksForTimeline, setSelectedConceptStocksForTimeline] = useState([]);
|
||||||
// 股票行情数据状态
|
// 股票行情数据状态
|
||||||
const [stockMarketData, setStockMarketData] = useState({});
|
const [stockMarketData, setStockMarketData] = useState({});
|
||||||
const [loadingStockData, setLoadingStockData] = useState(false);
|
const [loadingStockData, setLoadingStockData] = useState(false);
|
||||||
@@ -367,7 +368,7 @@ const ConceptCenter = () => {
|
|||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
// 打开内容模态框(新闻和研报)- 需要Max版权限
|
// 打开内容模态框(新闻和研报)- 需要Max版权限
|
||||||
const handleViewContent = (e, conceptName, conceptId) => {
|
const handleViewContent = (e, conceptName, conceptId, stocks = []) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 检查历史时间轴权限
|
// 检查历史时间轴权限
|
||||||
@@ -383,6 +384,7 @@ const ConceptCenter = () => {
|
|||||||
|
|
||||||
setSelectedConceptForContent(conceptName);
|
setSelectedConceptForContent(conceptName);
|
||||||
setSelectedConceptId(conceptId);
|
setSelectedConceptId(conceptId);
|
||||||
|
setSelectedConceptStocksForTimeline(stocks || []);
|
||||||
setIsTimelineModalOpen(true);
|
setIsTimelineModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1110,7 +1112,7 @@ const ConceptCenter = () => {
|
|||||||
bg="purple.500"
|
bg="purple.500"
|
||||||
color="white"
|
color="white"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id)}
|
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id, concept.stocks)}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
px={{ base: 2, md: 4 }}
|
px={{ base: 2, md: 4 }}
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
@@ -1296,7 +1298,7 @@ const ConceptCenter = () => {
|
|||||||
leftIcon={<FaChartLine />}
|
leftIcon={<FaChartLine />}
|
||||||
bg="purple.500"
|
bg="purple.500"
|
||||||
color="white"
|
color="white"
|
||||||
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id)}
|
onClick={(e) => handleViewContent(e, concept.concept, concept.concept_id, concept.stocks)}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
boxShadow="0 4px 12px rgba(139, 92, 246, 0.4)"
|
boxShadow="0 4px 12px rgba(139, 92, 246, 0.4)"
|
||||||
_hover={{ bg: 'purple.400', boxShadow: '0 6px 16px rgba(139, 92, 246, 0.5)' }}
|
_hover={{ bg: 'purple.400', boxShadow: '0 6px 16px rgba(139, 92, 246, 0.5)' }}
|
||||||
@@ -1769,12 +1771,12 @@ const ConceptCenter = () => {
|
|||||||
setViewMode('force3d');
|
setViewMode('force3d');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
bg={viewMode === 'force3d' ? 'yellow.500' : 'transparent'}
|
bg={viewMode === 'force3d' ? 'purple.500' : 'transparent'}
|
||||||
color={viewMode === 'force3d' ? 'gray.900' : 'whiteAlpha.700'}
|
color={viewMode === 'force3d' ? 'white' : 'whiteAlpha.700'}
|
||||||
borderColor="whiteAlpha.300"
|
borderColor="whiteAlpha.300"
|
||||||
_hover={{
|
_hover={{
|
||||||
bg: viewMode === 'force3d' ? 'yellow.400' : 'whiteAlpha.100',
|
bg: viewMode === 'force3d' ? 'purple.400' : 'whiteAlpha.100',
|
||||||
boxShadow: viewMode === 'force3d' ? '0 0 10px rgba(234, 179, 8, 0.4)' : 'none',
|
boxShadow: viewMode === 'force3d' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
|
||||||
}}
|
}}
|
||||||
aria-label="概念矩形树图"
|
aria-label="概念矩形树图"
|
||||||
/>
|
/>
|
||||||
@@ -1788,12 +1790,12 @@ const ConceptCenter = () => {
|
|||||||
setViewMode('hierarchy');
|
setViewMode('hierarchy');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
bg={viewMode === 'hierarchy' ? 'yellow.500' : 'transparent'}
|
bg={viewMode === 'hierarchy' ? 'purple.500' : 'transparent'}
|
||||||
color={viewMode === 'hierarchy' ? 'gray.900' : 'whiteAlpha.700'}
|
color={viewMode === 'hierarchy' ? 'white' : 'whiteAlpha.700'}
|
||||||
borderColor="whiteAlpha.300"
|
borderColor="whiteAlpha.300"
|
||||||
_hover={{
|
_hover={{
|
||||||
bg: viewMode === 'hierarchy' ? 'yellow.400' : 'whiteAlpha.100',
|
bg: viewMode === 'hierarchy' ? 'purple.400' : 'whiteAlpha.100',
|
||||||
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(234, 179, 8, 0.4)' : 'none',
|
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
|
||||||
}}
|
}}
|
||||||
aria-label="层级图"
|
aria-label="层级图"
|
||||||
/>
|
/>
|
||||||
@@ -1807,12 +1809,12 @@ const ConceptCenter = () => {
|
|||||||
setViewMode('list');
|
setViewMode('list');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
bg={viewMode === 'list' ? 'yellow.500' : 'transparent'}
|
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
|
||||||
color={viewMode === 'list' ? 'gray.900' : 'whiteAlpha.700'}
|
color={viewMode === 'list' ? 'white' : 'whiteAlpha.700'}
|
||||||
borderColor="whiteAlpha.300"
|
borderColor="whiteAlpha.300"
|
||||||
_hover={{
|
_hover={{
|
||||||
bg: viewMode === 'list' ? 'yellow.400' : 'whiteAlpha.100',
|
bg: viewMode === 'list' ? 'purple.400' : 'whiteAlpha.100',
|
||||||
boxShadow: viewMode === 'list' ? '0 0 10px rgba(234, 179, 8, 0.4)' : 'none',
|
boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.4)' : 'none',
|
||||||
}}
|
}}
|
||||||
aria-label="列表视图"
|
aria-label="列表视图"
|
||||||
/>
|
/>
|
||||||
@@ -2057,6 +2059,7 @@ const ConceptCenter = () => {
|
|||||||
onClose={() => setIsTimelineModalOpen(false)}
|
onClose={() => setIsTimelineModalOpen(false)}
|
||||||
conceptName={selectedConceptForContent}
|
conceptName={selectedConceptForContent}
|
||||||
conceptId={selectedConceptId}
|
conceptId={selectedConceptId}
|
||||||
|
stocks={selectedConceptStocksForTimeline}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 订阅升级Modal */}
|
{/* 订阅升级Modal */}
|
||||||
|
|||||||
Reference in New Issue
Block a user