community增加事件详情
This commit is contained in:
54
app.py
54
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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user