community增加事件详情

This commit is contained in:
2026-01-08 17:27:29 +08:00
parent 955bf9e34b
commit e3b13324a3
2 changed files with 164 additions and 84 deletions

54
app.py
View File

@@ -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,
}
})

View File

@@ -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 (
<Center h="300px">
<VStack spacing={4}>
@@ -251,14 +277,6 @@ const ThemeCometChart = ({ onThemeSelect }) => {
);
}
if (!data.themes || data.themes.length === 0) {
return (
<Center h="300px">
<Text color="whiteAlpha.500">暂无数据</Text>
</Center>
);
}
return (
<Box
bg="linear-gradient(135deg, rgba(15,15,30,0.6) 0%, rgba(25,25,50,0.6) 100%)"
@@ -271,28 +289,83 @@ const ThemeCometChart = ({ onThemeSelect }) => {
{/* 标题栏 */}
<HStack spacing={3} mb={2}>
<Box p={2} bg="rgba(255,215,0,0.15)" borderRadius="lg" border="1px solid rgba(255,215,0,0.3)">
<RocketOutlined style={{ color: '#FFD700', fontSize: '18px' }} />
<ThunderboltOutlined style={{ color: '#FFD700', fontSize: '18px' }} />
</Box>
<VStack align="start" spacing={0}>
<Text fontSize="md" fontWeight="bold" color="#FFD700">
AI 舆情 · 时空决策驾驶舱
</Text>
<VStack align="start" spacing={0} flex={1}>
<HStack>
<Text fontSize="md" fontWeight="bold" color="#FFD700">
连板情绪监测
</Text>
{loading && <Spinner size="xs" color="yellow.400" />}
</HStack>
<Text fontSize="xs" color="whiteAlpha.500">
题材流星图 · {data.latestDate}
{data.currentDate}
</Text>
</VStack>
</HStack>
{/* 图表区域 */}
<Box h="calc(100% - 60px)">
<ReactECharts
option={chartOption}
style={{ height: '100%', width: '100%' }}
onEvents={onChartEvents}
opts={{ renderer: 'canvas' }}
/>
<Box h="calc(100% - 100px)">
{data.themes.length > 0 ? (
<ReactECharts
option={chartOption}
style={{ height: '100%', width: '100%' }}
onEvents={onChartEvents}
opts={{ renderer: 'canvas' }}
/>
) : (
<Center h="100%">
<Text color="whiteAlpha.500">暂无数据</Text>
</Center>
)}
</Box>
{/* 时间滑动条 */}
{data.availableDates.length > 1 && (
<Box px={2} pt={2}>
<HStack spacing={3}>
<Text fontSize="xs" color="whiteAlpha.500" whiteSpace="nowrap">
{data.availableDates[data.availableDates.length - 1]?.formatted?.slice(5) || ''}
</Text>
<ChakraTooltip
hasArrow
bg="gray.700"
color="white"
placement="top"
isOpen={showTooltip}
label={currentSliderDate}
>
<Slider
aria-label="date-slider"
min={0}
max={data.availableDates.length - 1}
step={1}
value={sliderIndex}
onChange={handleSliderChange}
onChangeEnd={handleSliderChangeEnd}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
isReversed
>
<SliderTrack bg="whiteAlpha.200" h="6px" borderRadius="full">
<SliderFilledTrack bg="linear-gradient(90deg, #FFD700, #FFA500)" />
</SliderTrack>
<SliderThumb
boxSize={4}
bg="#FFD700"
border="2px solid"
borderColor="orange.400"
_focus={{ boxShadow: '0 0 0 3px rgba(255,215,0,0.3)' }}
/>
</Slider>
</ChakraTooltip>
<Text fontSize="xs" color="whiteAlpha.500" whiteSpace="nowrap">
{data.availableDates[0]?.formatted?.slice(5) || ''}
</Text>
</HStack>
</Box>
)}
{/* 详情弹窗 */}
<Modal isOpen={isOpen} onClose={onClose} size="xl" isCentered>
<ModalOverlay bg="blackAlpha.700" />
@@ -304,7 +377,6 @@ const ThemeCometChart = ({ onThemeSelect }) => {
<ModalBody pb={6} overflowY="auto">
{selectedTheme && (
<VStack spacing={4} align="stretch">
{/* 趋势表格 */}
<Box>
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>历史数据</Text>
<Table size="sm" variant="simple">
@@ -339,10 +411,9 @@ const ThemeCometChart = ({ onThemeSelect }) => {
</Table>
</Box>
{/* 今日涨停股票 */}
<Box>
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>
今日涨停股票{selectedTheme.stocks?.length || 0}
涨停股票{selectedTheme.stocks?.length || 0}
</Text>
<Box maxH="200px" overflowY="auto">
<Table size="sm" variant="simple">
@@ -370,7 +441,6 @@ const ThemeCometChart = ({ onThemeSelect }) => {
</Box>
</Box>
{/* 匹配的板块 */}
{selectedTheme.matchedSectors?.length > 0 && (
<Box>
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>匹配板块</Text>