diff --git a/app.py b/app.py index 931591e6..556cc977 100755 --- a/app.py +++ b/app.py @@ -10754,6 +10754,240 @@ def get_calendar_combined_data(): }), 500 +@app.route('/api/v1/zt/theme-scatter', methods=['GET']) +def get_zt_theme_scatter(): + """获取题材流星图数据(散点图) + + 基于词频关键词合并同义概念,统计最近5个交易日数据 + + 返回: + { + success: true, + data: { + latestDate: "2026-01-07", + themes: [ + { + label: "光刻胶", + color: "#F59E0B", + x: 5, // 最高连板数(辨识度) + y: 12, // 涨停家数(热度) + countTrend: 3, // 较前日变化 + boardTrend: 1, // 连板变化 + status: "rising", // rising/declining/lurking/clustering + history: [...], // 5日历史数据 + stocks: [...], // 今日涨停股票 + matchedSectors: [] // 匹配的原始板块名 + }, + ... + ] + } + } + """ + import os + import json + import re + + # 核心主题词配置(基于词频,合并同义概念) + THEME_KEYWORDS = [ + {'keyword': '光刻', 'aliases': ['光刻胶', '光刻机'], 'label': '光刻胶', 'color': '#F59E0B'}, + {'keyword': '航天', 'aliases': ['商业航天', '卫星导航', '卫星互联', '低轨卫星'], 'label': '商业航天', 'color': '#FF6B6B'}, + {'keyword': '脑机', 'aliases': ['脑机接口', '人脑工程', '脑科学'], 'label': '脑机接口', 'color': '#A78BFA'}, + {'keyword': '算力', 'aliases': ['算力', 'AI算力', '智算中心', '东数西算'], 'label': 'AI算力', 'color': '#3B82F6'}, + {'keyword': '机器人', 'aliases': ['机器人', '人形机器', '智能机器'], 'label': '机器人', 'color': '#4ECDC4'}, + {'keyword': '液冷', 'aliases': ['液冷', '液冷服务器', '液冷技术'], 'label': 'AI液冷', 'color': '#06B6D4'}, + {'keyword': '军工', 'aliases': ['军工', '军工信息', '国防军工'], 'label': '军工', 'color': '#EF4444'}, + {'keyword': '核', 'aliases': ['核聚变', '可控核变', '核电', '核能'], 'label': '核聚变', 'color': '#EC4899'}, + {'keyword': '人工智能', 'aliases': ['人工智能', 'AI应用', 'AIGC', 'AI+'], 'label': '人工智能', 'color': '#8B5CF6'}, + {'keyword': '国产替代', 'aliases': ['国产替代', '国产软件', '信创'], 'label': '国产替代', 'color': '#10B981'}, + {'keyword': '固态电池', 'aliases': ['固态电池', '锂电池', '钠电池'], 'label': '固态电池', 'color': '#22C55E'}, + {'keyword': '数据中心', 'aliases': ['数据中心', 'IDC', '算力中心'], 'label': '数据中心', 'color': '#0EA5E9'}, + ] + + # 噪音板块黑名单 + NOISE_SECTORS = ['公告', '业绩预告', '重大合同', '重组', '股权激励', '其他', + '增持', '回购', '解禁', '减持', '限售股', '转债'] + + try: + days = request.args.get('days', 5, type=int) + days = min(max(days, 1), 10) # 限制1-10天 + + # 查找数据目录 + possible_paths = [ + os.path.join(app.static_folder or 'static', 'data', 'zt'), + os.path.join('public', 'data', 'zt'), + os.path.join('data', 'zt'), + '/var/www/vf_react/data/zt', + ] + + zt_dir = None + for path in possible_paths: + if os.path.isdir(path): + zt_dir = path + break + + if not zt_dir: + return jsonify({'success': False, 'error': '找不到涨停数据目录'}), 404 + + # 读取日期列表 + dates_file = os.path.join(zt_dir, 'dates.json') + if not os.path.exists(dates_file): + return jsonify({'success': False, 'error': '找不到日期数据'}), 404 + + with open(dates_file, 'r', encoding='utf-8') as f: + dates_data = json.load(f) + + recent_dates = dates_data.get('dates', [])[:days] + if not recent_dates: + return jsonify({'success': False, 'error': '无可用日期数据'}), 404 + + # 读取每日数据 + daily_data_list = [] + daily_dir = os.path.join(zt_dir, 'daily') + + for date_info in recent_dates: + date_str = date_info.get('date', '') + daily_file = os.path.join(daily_dir, f'{date_str}.json') + + if os.path.exists(daily_file): + try: + with open(daily_file, 'r', encoding='utf-8') as f: + data = json.load(f) + daily_data_list.append({ + 'date': date_str, + 'formatted_date': date_info.get('formatted_date', ''), + 'sector_data': data.get('sector_data', {}), + 'stocks': data.get('stocks', []), + 'word_freq': data.get('word_freq_data', []), + }) + except Exception as e: + print(f"[theme-scatter] 读取 {daily_file} 失败: {e}") + + if not daily_data_list: + return jsonify({'success': False, 'error': '无法读取每日数据'}), 404 + + def match_sectors_by_keyword(sector_data, theme_config): + """基于关键词匹配板块""" + matched = [] + for sector_name, sector_info in sector_data.items(): + # 过滤噪音板块 + if any(noise in sector_name for noise in NOISE_SECTORS): + continue + # 检查是否匹配任何别名 + is_match = any( + alias in sector_name or sector_name in alias + for alias in theme_config['aliases'] + ) + if is_match: + matched.append({ + 'name': sector_name, + 'stock_codes': sector_info.get('stock_codes', []), + 'count': sector_info.get('count', 0), + }) + return matched + + def get_continuous_days(stock): + """提取连板天数""" + cd = stock.get('continuous_days', '1') + match = re.search(r'(\d+)', str(cd)) + return int(match.group(1)) if match else 1 + + # 处理每个主题 + themes = [] + latest_date = daily_data_list[0]['formatted_date'] + + for theme_config in THEME_KEYWORDS: + # 收集每日数据 + history = [] + + for day_data in daily_data_list: + matched_sectors = match_sectors_by_keyword(day_data['sector_data'], theme_config) + + # 合并所有匹配板块的股票(去重) + all_stock_codes = set() + for sector in matched_sectors: + all_stock_codes.update(sector['stock_codes']) + + # 计算最高连板数 + max_board = 1 + stock_details = [] + for code in all_stock_codes: + stock_info = next((s for s in day_data['stocks'] if s.get('scode') == code), None) + if stock_info: + days_val = get_continuous_days(stock_info) + max_board = max(max_board, days_val) + stock_details.append({ + 'scode': stock_info.get('scode', code), + 'sname': stock_info.get('sname', code), + 'continuous_days': days_val, + 'brief': stock_info.get('brief', '')[:100] if stock_info.get('brief') else '', + }) + else: + stock_details.append({'scode': code, 'sname': code, 'continuous_days': 1, 'brief': ''}) + + # 按连板数排序 + stock_details.sort(key=lambda x: x['continuous_days'], reverse=True) + + history.append({ + 'date': day_data['formatted_date'], + 'count': len(all_stock_codes), + 'maxBoard': max_board, + 'stocks': stock_details, + 'matchedSectors': [s['name'] for s in matched_sectors], + }) + + # 跳过无数据的主题 + if not history or history[0]['count'] == 0: + continue + + latest = history[0] + previous = history[1] if len(history) > 1 else {'count': 0, 'maxBoard': 1} + + count_trend = latest['count'] - previous['count'] + board_trend = latest['maxBoard'] - previous['maxBoard'] + + # 判断状态 + if latest['maxBoard'] >= 3 and count_trend > 0: + status = 'rising' + elif latest['maxBoard'] >= 3 and count_trend < 0: + status = 'declining' + elif latest['maxBoard'] < 3 and count_trend > 0: + status = 'lurking' + else: + status = 'clustering' + + themes.append({ + 'label': theme_config['label'], + 'color': theme_config['color'], + 'x': latest['maxBoard'], + 'y': latest['count'], + 'countTrend': count_trend, + 'boardTrend': board_trend, + 'status': status, + 'history': history, + 'stocks': latest['stocks'], + 'matchedSectors': latest['matchedSectors'], + }) + + # 按热度排序 + themes.sort(key=lambda x: x['y'], reverse=True) + + return jsonify({ + 'success': True, + 'data': { + 'latestDate': latest_date, + 'themes': themes, + } + }) + + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @app.route('/api/v1/calendar/events', methods=['GET']) def get_calendar_events(): """获取指定日期的事件列表""" diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js index 4d48286f..e2583ff0 100644 --- a/src/views/Community/components/ThemeCometChart.js +++ b/src/views/Community/components/ThemeCometChart.js @@ -1,8 +1,8 @@ /** * ThemeCometChart - 题材流星图(散点图版本) - * X轴:辨识度(最高板高度,即板块内最大连板数) + * X轴:辨识度(最高板高度) * Y轴:板块热度(涨停家数) - * 散点大小/颜色:表示不同状态(主升、退潮、潜伏、抱团) + * 数据由后端 API /api/v1/zt/theme-scatter 提供 */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { @@ -13,206 +13,88 @@ import { Spinner, Center, useToast, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + useDisclosure, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Badge, } from '@chakra-ui/react'; import ReactECharts from 'echarts-for-react'; import { RocketOutlined } from '@ant-design/icons'; +import { getApiBase } from '@/utils/env'; -// 板块状态分类 -const SECTOR_STATUS = { - RISING: { name: '主升', color: '#FF4D4F', icon: '🔥' }, // 高辨识度+高热度+上升趋势 - DECLINING: { name: '退潮', color: '#52C41A', icon: '❄️' }, // 高辨识度+热度下降 - LURKING: { name: '潜伏', color: '#1890FF', icon: '●' }, // 低辨识度+热度上升 - CLUSTERING: { name: '抱团', color: '#722ED1', icon: '●' }, // 中等辨识度+稳定热度 -}; - -// 噪音板块黑名单(过滤无意义的板块) -const NOISE_SECTORS = [ - '公告', '业绩预告', '重大合同签署', '重组', '股权激励', - '增持', '回购', '解禁', '减持', '限售股解禁', -]; - -/** - * 从 public/data/zt 读取最近两天数据用于比较趋势 - */ -const fetchSectorData = async () => { - try { - // 获取日期列表 - const datesRes = await fetch('/data/zt/dates.json'); - const datesData = await datesRes.json(); - const recentDates = datesData.dates.slice(0, 2); // 最近两天 - - if (recentDates.length === 0) return { today: null, yesterday: null }; - - // 获取今日数据 - const todayRes = await fetch(`/data/zt/daily/${recentDates[0].date}.json`); - const todayData = await todayRes.json(); - - // 获取昨日数据(用于计算趋势) - let yesterdayData = null; - if (recentDates.length > 1) { - try { - const yesterdayRes = await fetch(`/data/zt/daily/${recentDates[1].date}.json`); - yesterdayData = await yesterdayRes.json(); - } catch { - // 忽略 - } - } - - return { - today: { - date: recentDates[0].formatted_date, - sectorData: todayData.sector_data || {}, - stocks: todayData.stocks || [], - }, - yesterday: yesterdayData ? { - date: recentDates[1].formatted_date, - sectorData: yesterdayData.sector_data || {}, - stocks: yesterdayData.stocks || [], - } : null, - }; - } catch (error) { - console.error('获取板块数据失败:', error); - return { today: null, yesterday: null }; - } -}; - -/** - * 处理板块数据,计算散点图坐标 - */ -const processSectorData = (data) => { - if (!data.today) return []; - - const { sectorData, stocks } = data.today; - const yesterdaySectors = data.yesterday?.sectorData || {}; - - // 构建股票代码到连板数的映射 - const stockContinuousDays = {}; - stocks.forEach((stock) => { - // continuous_days 可能是 "1板" 或数字 - let days = 1; - if (stock.continuous_days) { - const match = String(stock.continuous_days).match(/(\d+)/); - if (match) days = parseInt(match[1]); - } - stockContinuousDays[stock.scode] = days; - }); - - const sectors = []; - - Object.entries(sectorData).forEach(([sectorName, sectorInfo]) => { - // 过滤噪音板块 - if (NOISE_SECTORS.some((noise) => sectorName.includes(noise))) return; - - const stockCodes = sectorInfo.stock_codes || []; - const count = sectorInfo.count || stockCodes.length; - - // 计算该板块的最高连板数(辨识度) - let maxContinuousDays = 1; - stockCodes.forEach((code) => { - const days = stockContinuousDays[code] || 1; - if (days > maxContinuousDays) maxContinuousDays = days; - }); - - // 获取昨日数据计算趋势 - const yesterdayCount = yesterdaySectors[sectorName]?.count || 0; - const trend = count - yesterdayCount; - - // 判断板块状态 - let status; - if (maxContinuousDays >= 3 && trend > 0) { - status = SECTOR_STATUS.RISING; // 主升:高辨识度+上升 - } else if (maxContinuousDays >= 3 && trend < 0) { - status = SECTOR_STATUS.DECLINING; // 退潮:高辨识度+下降 - } else if (maxContinuousDays < 3 && trend > 0) { - status = SECTOR_STATUS.LURKING; // 潜伏:低辨识度+上升 - } else { - status = SECTOR_STATUS.CLUSTERING; // 抱团:其他 - } - - sectors.push({ - name: sectorName, - x: maxContinuousDays, // X轴:最高板高度 - y: count, // Y轴:涨停家数 - trend, - status, - stockCodes, - stocks: stockCodes.map((code) => { - const stockInfo = stocks.find((s) => s.scode === code); - return stockInfo || { scode: code, sname: code }; - }), - }); - }); - - // 按热度排序 - sectors.sort((a, b) => b.y - a.y); - - return sectors; +// 板块状态配置 +const STATUS_CONFIG = { + rising: { name: '主升', color: '#FF4D4F' }, + declining: { name: '退潮', color: '#52C41A' }, + lurking: { name: '潜伏', color: '#1890FF' }, + clustering: { name: '抱团', color: '#722ED1' }, }; /** * 生成 ECharts 配置 */ -const generateChartOption = (sectors) => { - if (!sectors || sectors.length === 0) return {}; +const generateChartOption = (themes) => { + if (!themes || themes.length === 0) return {}; // 按状态分组 const groupedData = { - [SECTOR_STATUS.RISING.name]: [], - [SECTOR_STATUS.DECLINING.name]: [], - [SECTOR_STATUS.LURKING.name]: [], - [SECTOR_STATUS.CLUSTERING.name]: [], + 主升: [], + 退潮: [], + 潜伏: [], + 抱团: [], }; - sectors.forEach((sector) => { - groupedData[sector.status.name].push({ - name: sector.name, - value: [sector.x, sector.y], - trend: sector.trend, - stockCount: sector.y, - maxBoard: sector.x, - stocks: sector.stocks, + themes.forEach((theme) => { + const statusName = STATUS_CONFIG[theme.status]?.name || '抱团'; + groupedData[statusName].push({ + name: theme.label, + value: [theme.x, theme.y], + countTrend: theme.countTrend, + boardTrend: theme.boardTrend, + themeColor: theme.color, + history: theme.history, }); }); // 创建系列 - const series = Object.entries(SECTOR_STATUS).map(([key, status]) => ({ - name: status.name, + const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({ + name: config.name, type: 'scatter', - data: groupedData[status.name], - symbolSize: (data) => { - // 根据热度调整大小 - const size = Math.max(20, Math.min(60, data[1] * 4)); - return size; - }, + data: groupedData[config.name] || [], + symbolSize: (data) => Math.max(25, Math.min(70, data[1] * 3.5)), itemStyle: { - color: status.color, - shadowBlur: 10, - shadowColor: status.color, - opacity: 0.8, + color: (params) => params.data.themeColor || config.color, + shadowBlur: 12, + shadowColor: (params) => params.data.themeColor || config.color, + opacity: 0.85, }, emphasis: { - itemStyle: { - opacity: 1, - shadowBlur: 20, - }, - label: { - show: true, - }, + itemStyle: { opacity: 1, shadowBlur: 25 }, }, label: { show: true, formatter: (params) => params.data.name, position: 'right', color: '#fff', - fontSize: 11, + fontSize: 12, + fontWeight: 'bold', textShadowColor: 'rgba(0,0,0,0.8)', textShadowBlur: 4, }, })); - // 计算坐标轴范围 - const maxX = Math.max(...sectors.map((s) => s.x), 5) + 1; - const maxY = Math.max(...sectors.map((s) => s.y), 10) + 2; + const maxX = Math.max(...themes.map((t) => t.x), 5) + 1; + const maxY = Math.max(...themes.map((t) => t.y), 10) + 3; return { backgroundColor: 'transparent', @@ -221,60 +103,58 @@ const generateChartOption = (sectors) => { backgroundColor: 'rgba(15, 15, 30, 0.95)', borderColor: 'rgba(255, 215, 0, 0.3)', borderWidth: 1, - textStyle: { - color: '#fff', - }, + textStyle: { color: '#fff' }, formatter: (params) => { - const { name, value, trend, stocks } = params.data; - const trendText = trend > 0 ? `+${trend}` : trend; - const trendIcon = trend > 0 ? '🔥' : trend < 0 ? '❄️' : '➡️'; + const { name, value, countTrend, boardTrend, history } = params.data; + const countTrendText = countTrend > 0 ? `+${countTrend}` : countTrend; + const boardTrendText = boardTrend > 0 ? `+${boardTrend}` : boardTrend; + const countIcon = countTrend > 0 ? '🔥' : countTrend < 0 ? '❄️' : '➡️'; - // 显示前5只股票 - const topStocks = stocks.slice(0, 5); - const stockList = topStocks - .map((s) => `${s.sname || s.scode}`) - .join('、'); + const historyText = (history || []) + .slice(0, 5) + .map((h) => `${h.date?.slice(5) || ''}: ${h.count}家/${h.maxBoard}板`) + .join('
'); return `
- ${name} ${trendIcon} + ${name} ${countIcon}
涨停家数: ${value[1]}家 - (${trendText}) + (${countTrendText})
最高连板: ${value[0]}板 + (${boardTrendText})
-
- ${stockList}${stocks.length > 5 ? '...' : ''} +
+
近5日趋势:
+
${historyText}
+
点击查看详情
`; }, }, legend: { show: true, - top: 10, + top: 5, right: 10, orient: 'horizontal', - textStyle: { - color: 'rgba(255, 255, 255, 0.7)', - fontSize: 11, - }, - itemWidth: 14, - itemHeight: 14, - data: Object.values(SECTOR_STATUS).map((s) => ({ + textStyle: { color: 'rgba(255, 255, 255, 0.7)', fontSize: 11 }, + itemWidth: 12, + itemHeight: 12, + data: Object.values(STATUS_CONFIG).map((s) => ({ name: s.name, icon: 'circle', itemStyle: { color: s.color }, })), }, grid: { - left: '8%', - right: '5%', - top: '15%', + left: '10%', + right: '8%', + top: '12%', bottom: '15%', containLabel: true, }, @@ -283,52 +163,25 @@ const generateChartOption = (sectors) => { name: '辨识度(最高板)', nameLocation: 'middle', nameGap: 30, - nameTextStyle: { - color: 'rgba(255, 255, 255, 0.6)', - fontSize: 12, - }, + nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 }, min: 0, max: maxX, interval: 1, - axisLine: { - lineStyle: { - color: 'rgba(255, 255, 255, 0.2)', - }, - }, - axisLabel: { - color: 'rgba(255, 255, 255, 0.6)', - fontSize: 11, - formatter: '{value}板', - }, - splitLine: { - lineStyle: { - color: 'rgba(255, 255, 255, 0.05)', - }, - }, + axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } }, + axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11, formatter: '{value}板' }, + splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }, }, yAxis: { type: 'value', name: '板块热度(家数)', nameLocation: 'middle', nameGap: 40, - nameTextStyle: { - color: 'rgba(255, 255, 255, 0.6)', - fontSize: 12, - }, + nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 }, min: 0, max: maxY, - axisLine: { - show: false, - }, - axisLabel: { - color: 'rgba(255, 255, 255, 0.6)', - fontSize: 11, - }, - splitLine: { - lineStyle: { - color: 'rgba(255, 255, 255, 0.05)', - }, - }, + axisLine: { show: false }, + axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 }, + splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }, }, series, }; @@ -339,28 +192,30 @@ const generateChartOption = (sectors) => { */ const ThemeCometChart = ({ onThemeSelect }) => { const [loading, setLoading] = useState(true); - const [sectors, setSectors] = useState([]); - const [dateInfo, setDateInfo] = useState(''); + const [data, setData] = useState({ themes: [], latestDate: '' }); + const [selectedTheme, setSelectedTheme] = useState(null); + const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); - // 加载数据 useEffect(() => { const loadData = async () => { setLoading(true); try { - const data = await fetchSectorData(); - const processed = processSectorData(data); - setSectors(processed); - if (data.today) { - setDateInfo(data.today.date); + const apiBase = getApiBase(); + const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`); + const result = await response.json(); + + if (result.success && result.data) { + setData({ + themes: result.data.themes || [], + latestDate: result.data.latestDate || '', + }); + } else { + throw new Error(result.error || '加载失败'); } } catch (error) { - console.error('加载板块数据失败:', error); - toast({ - title: '加载数据失败', - status: 'error', - duration: 3000, - }); + console.error('加载题材数据失败:', error); + toast({ title: '加载数据失败', description: error.message, status: 'error', duration: 3000 }); } finally { setLoading(false); } @@ -368,53 +223,38 @@ const ThemeCometChart = ({ onThemeSelect }) => { loadData(); }, [toast]); - // 图表配置 - const chartOption = useMemo(() => { - return generateChartOption(sectors); - }, [sectors]); + const chartOption = useMemo(() => generateChartOption(data.themes), [data.themes]); - // 点击事件 const handleChartClick = useCallback( (params) => { - if (params.data && onThemeSelect) { - const sector = sectors.find((s) => s.name === params.data.name); - if (sector) { - onThemeSelect({ - theme: { label: sector.name, color: sector.status.color }, - stocks: sector.stocks.map((s) => ({ - ...s, - _continuousDays: 1, - })), - date: dateInfo, - }); + if (params.data) { + const theme = data.themes.find((t) => t.label === params.data.name); + if (theme) { + setSelectedTheme(theme); + onOpen(); } } }, - [sectors, dateInfo, onThemeSelect] + [data.themes, onOpen] ); - const onChartEvents = useMemo( - () => ({ - click: handleChartClick, - }), - [handleChartClick] - ); + const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]); if (loading) { return (
- 加载板块数据... + 加载数据...
); } - if (!sectors || sectors.length === 0) { + if (!data.themes || data.themes.length === 0) { return (
- 暂无板块数据 + 暂无数据
); } @@ -430,12 +270,7 @@ const ThemeCometChart = ({ onThemeSelect }) => { > {/* 标题栏 */} - + @@ -443,7 +278,7 @@ const ThemeCometChart = ({ onThemeSelect }) => { AI 舆情 · 时空决策驾驶舱 - 题材流星图 · {dateInfo} + 题材流星图 · {data.latestDate} @@ -457,6 +292,102 @@ const ThemeCometChart = ({ onThemeSelect }) => { opts={{ renderer: 'canvas' }} /> + + {/* 详情弹窗 */} + + + + + {selectedTheme?.label} - 近5日趋势 + + + + {selectedTheme && ( + + {/* 趋势表格 */} + + 历史数据 + + + + + + + + + + + {selectedTheme.history?.map((h, idx) => { + const prev = selectedTheme.history[idx + 1]; + const countChange = prev ? h.count - prev.count : 0; + return ( + + + + + + + ); + })} + +
日期涨停家数最高连板变化
{h.date}{h.count}家{h.maxBoard}板 + {countChange !== 0 && ( + 0 ? 'green' : 'red'}> + {countChange > 0 ? '+' : ''}{countChange} + + )} +
+
+ + {/* 今日涨停股票 */} + + + 今日涨停股票({selectedTheme.stocks?.length || 0}只) + + + + + + + + + + + + {selectedTheme.stocks?.slice(0, 20).map((stock) => ( + + + + + + ))} + +
代码名称连板
{stock.scode}{stock.sname} + = 3 ? 'red' : 'gray'}> + {stock.continuous_days}板 + +
+
+
+ + {/* 匹配的板块 */} + {selectedTheme.matchedSectors?.length > 0 && ( + + 匹配板块 + + {selectedTheme.matchedSectors.map((sector) => ( + + {sector} + + ))} + + + )} +
+ )} +
+
+
); };