diff --git a/app.py b/app.py index 556cc977..e0d18b5a 100755 --- a/app.py +++ b/app.py @@ -10758,28 +10758,19 @@ def get_calendar_combined_data(): def get_zt_theme_scatter(): """获取题材流星图数据(散点图) - 基于词频关键词合并同义概念,统计最近5个交易日数据 + 基于词频关键词合并同义概念,统计指定日期的数据 + + 参数: + date: 指定日期(YYYYMMDD格式,可选,默认最新) + days: 用于计算趋势的天数(默认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: [] // 匹配的原始板块名 - }, - ... - ] + currentDate: "2026-01-07", + availableDates: ["20260107", "20260106", ...], // 可选日期列表 + themes: [...] } } """ @@ -10808,8 +10799,9 @@ def get_zt_theme_scatter(): '增持', '回购', '解禁', '减持', '限售股', '转债'] try: - days = request.args.get('days', 5, type=int) - days = min(max(days, 1), 10) # 限制1-10天 + target_date = request.args.get('date', '') # 指定日期 + trend_days = request.args.get('days', 5, type=int) + trend_days = min(max(trend_days, 1), 10) # 限制1-10天 # 查找数据目录 possible_paths = [ @@ -10836,10 +10828,27 @@ def get_zt_theme_scatter(): with open(dates_file, 'r', encoding='utf-8') as f: dates_data = json.load(f) - recent_dates = dates_data.get('dates', [])[:days] + all_dates = dates_data.get('dates', []) + if not all_dates: + return jsonify({'success': False, 'error': '无可用日期数据'}), 404 + + # 获取最近一个月的日期列表(约22个交易日) + available_dates = all_dates[:25] + + # 确定当前查看的日期 + if target_date: + # 找到目标日期在列表中的位置 + target_idx = next((i for i, d in enumerate(all_dates) if d['date'] == target_date), 0) + else: + target_idx = 0 # 默认最新 + + # 获取从目标日期开始的 trend_days 天数据(用于计算趋势) + recent_dates = all_dates[target_idx:target_idx + trend_days] if not recent_dates: return jsonify({'success': False, 'error': '无可用日期数据'}), 404 + current_date_info = recent_dates[0] + # 读取每日数据 daily_data_list = [] daily_dir = os.path.join(zt_dir, 'daily') @@ -10893,7 +10902,7 @@ def get_zt_theme_scatter(): # 处理每个主题 themes = [] - latest_date = daily_data_list[0]['formatted_date'] + current_date = current_date_info.get('formatted_date', '') for theme_config in THEME_KEYWORDS: # 收集每日数据 @@ -10974,7 +10983,8 @@ def get_zt_theme_scatter(): return jsonify({ 'success': True, 'data': { - 'latestDate': latest_date, + 'currentDate': current_date, + 'availableDates': [{'date': d['date'], 'formatted': d['formatted_date']} for d in available_dates], 'themes': themes, } }) diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js index 3426d031..ca6abde2 100644 --- a/src/views/Community/components/ThemeCometChart.js +++ b/src/views/Community/components/ThemeCometChart.js @@ -1,8 +1,8 @@ /** - * ThemeCometChart - 题材流星图(散点图版本) + * ThemeCometChart - 连板情绪监测(散点图) * X轴:辨识度(最高板高度) * Y轴:板块热度(涨停家数) - * 数据由后端 API /api/v1/zt/theme-scatter 提供 + * 支持时间滑动条查看历史数据 */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { @@ -27,9 +27,14 @@ import { Th, Td, Badge, + Slider, + SliderTrack, + SliderFilledTrack, + SliderThumb, + Tooltip as ChakraTooltip, } from '@chakra-ui/react'; import ReactECharts from 'echarts-for-react'; -import { RocketOutlined } from '@ant-design/icons'; +import { ThunderboltOutlined } from '@ant-design/icons'; import { getApiBase } from '@utils/apiConfig'; // 板块状态配置 @@ -46,7 +51,6 @@ const STATUS_CONFIG = { const generateChartOption = (themes) => { if (!themes || themes.length === 0) return {}; - // 按状态分组 const groupedData = { 主升: [], 退潮: [], @@ -66,7 +70,6 @@ const generateChartOption = (themes) => { }); }); - // 创建系列 const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({ name: config.name, type: 'scatter', @@ -155,32 +158,32 @@ const generateChartOption = (themes) => { left: '10%', right: '8%', top: '12%', - bottom: '15%', + bottom: '8%', containLabel: true, }, xAxis: { type: 'value', name: '辨识度(最高板)', nameLocation: 'middle', - nameGap: 30, - nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 }, + nameGap: 25, + nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 }, 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}板' }, + axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 10, formatter: '{value}板' }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }, }, yAxis: { type: 'value', - name: '板块热度(家数)', + name: '热度(家数)', nameLocation: 'middle', - nameGap: 40, - nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 }, + nameGap: 35, + nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 }, min: 0, max: maxY, axisLine: { show: false }, - axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 }, + axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 10 }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }, }, series, @@ -192,37 +195,57 @@ const generateChartOption = (themes) => { */ const ThemeCometChart = ({ onThemeSelect }) => { const [loading, setLoading] = useState(true); - const [data, setData] = useState({ themes: [], latestDate: '' }); + const [data, setData] = useState({ themes: [], currentDate: '', availableDates: [] }); const [selectedTheme, setSelectedTheme] = useState(null); + const [sliderIndex, setSliderIndex] = useState(0); + const [showTooltip, setShowTooltip] = useState(false); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); - useEffect(() => { - const loadData = async () => { - setLoading(true); - try { - const apiBase = getApiBase(); - const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`); - const result = await response.json(); + // 加载指定日期的数据 + const loadData = useCallback(async (dateStr = '') => { + setLoading(true); + try { + const apiBase = getApiBase(); + const url = dateStr + ? `${apiBase}/api/v1/zt/theme-scatter?date=${dateStr}&days=5` + : `${apiBase}/api/v1/zt/theme-scatter?days=5`; + const response = await fetch(url); + 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: '加载数据失败', description: error.message, status: 'error', duration: 3000 }); - } finally { - setLoading(false); + if (result.success && result.data) { + setData({ + themes: result.data.themes || [], + currentDate: result.data.currentDate || '', + availableDates: result.data.availableDates || [], + }); + } else { + throw new Error(result.error || '加载失败'); } - }; - loadData(); + } catch (error) { + console.error('加载题材数据失败:', error); + toast({ title: '加载数据失败', description: error.message, status: 'error', duration: 3000 }); + } finally { + setLoading(false); + } }, [toast]); + // 初始加载 + useEffect(() => { + loadData(); + }, [loadData]); + + // 滑动条变化时加载对应日期数据 + const handleSliderChange = useCallback((value) => { + setSliderIndex(value); + }, []); + + const handleSliderChangeEnd = useCallback((value) => { + if (data.availableDates && data.availableDates[value]) { + loadData(data.availableDates[value].date); + } + }, [data.availableDates, loadData]); + const chartOption = useMemo(() => generateChartOption(data.themes), [data.themes]); const handleChartClick = useCallback( @@ -240,7 +263,10 @@ const ThemeCometChart = ({ onThemeSelect }) => { const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]); - if (loading) { + // 当前滑动条对应的日期 + const currentSliderDate = data.availableDates?.[sliderIndex]?.formatted || data.currentDate; + + if (loading && data.themes.length === 0) { return (