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():
|
def get_zt_theme_scatter():
|
||||||
"""获取题材流星图数据(散点图)
|
"""获取题材流星图数据(散点图)
|
||||||
|
|
||||||
基于词频关键词合并同义概念,统计最近5个交易日数据
|
基于词频关键词合并同义概念,统计指定日期的数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
date: 指定日期(YYYYMMDD格式,可选,默认最新)
|
||||||
|
days: 用于计算趋势的天数(默认5)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
{
|
{
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
latestDate: "2026-01-07",
|
currentDate: "2026-01-07",
|
||||||
themes: [
|
availableDates: ["20260107", "20260106", ...], // 可选日期列表
|
||||||
{
|
themes: [...]
|
||||||
label: "光刻胶",
|
|
||||||
color: "#F59E0B",
|
|
||||||
x: 5, // 最高连板数(辨识度)
|
|
||||||
y: 12, // 涨停家数(热度)
|
|
||||||
countTrend: 3, // 较前日变化
|
|
||||||
boardTrend: 1, // 连板变化
|
|
||||||
status: "rising", // rising/declining/lurking/clustering
|
|
||||||
history: [...], // 5日历史数据
|
|
||||||
stocks: [...], // 今日涨停股票
|
|
||||||
matchedSectors: [] // 匹配的原始板块名
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -10808,8 +10799,9 @@ def get_zt_theme_scatter():
|
|||||||
'增持', '回购', '解禁', '减持', '限售股', '转债']
|
'增持', '回购', '解禁', '减持', '限售股', '转债']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
days = request.args.get('days', 5, type=int)
|
target_date = request.args.get('date', '') # 指定日期
|
||||||
days = min(max(days, 1), 10) # 限制1-10天
|
trend_days = request.args.get('days', 5, type=int)
|
||||||
|
trend_days = min(max(trend_days, 1), 10) # 限制1-10天
|
||||||
|
|
||||||
# 查找数据目录
|
# 查找数据目录
|
||||||
possible_paths = [
|
possible_paths = [
|
||||||
@@ -10836,10 +10828,27 @@ def get_zt_theme_scatter():
|
|||||||
with open(dates_file, 'r', encoding='utf-8') as f:
|
with open(dates_file, 'r', encoding='utf-8') as f:
|
||||||
dates_data = json.load(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:
|
if not recent_dates:
|
||||||
return jsonify({'success': False, 'error': '无可用日期数据'}), 404
|
return jsonify({'success': False, 'error': '无可用日期数据'}), 404
|
||||||
|
|
||||||
|
current_date_info = recent_dates[0]
|
||||||
|
|
||||||
# 读取每日数据
|
# 读取每日数据
|
||||||
daily_data_list = []
|
daily_data_list = []
|
||||||
daily_dir = os.path.join(zt_dir, 'daily')
|
daily_dir = os.path.join(zt_dir, 'daily')
|
||||||
@@ -10893,7 +10902,7 @@ def get_zt_theme_scatter():
|
|||||||
|
|
||||||
# 处理每个主题
|
# 处理每个主题
|
||||||
themes = []
|
themes = []
|
||||||
latest_date = daily_data_list[0]['formatted_date']
|
current_date = current_date_info.get('formatted_date', '')
|
||||||
|
|
||||||
for theme_config in THEME_KEYWORDS:
|
for theme_config in THEME_KEYWORDS:
|
||||||
# 收集每日数据
|
# 收集每日数据
|
||||||
@@ -10974,7 +10983,8 @@ def get_zt_theme_scatter():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': {
|
'data': {
|
||||||
'latestDate': latest_date,
|
'currentDate': current_date,
|
||||||
|
'availableDates': [{'date': d['date'], 'formatted': d['formatted_date']} for d in available_dates],
|
||||||
'themes': themes,
|
'themes': themes,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* ThemeCometChart - 题材流星图(散点图版本)
|
* ThemeCometChart - 连板情绪监测(散点图)
|
||||||
* X轴:辨识度(最高板高度)
|
* X轴:辨识度(最高板高度)
|
||||||
* Y轴:板块热度(涨停家数)
|
* Y轴:板块热度(涨停家数)
|
||||||
* 数据由后端 API /api/v1/zt/theme-scatter 提供
|
* 支持时间滑动条查看历史数据
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -27,9 +27,14 @@ import {
|
|||||||
Th,
|
Th,
|
||||||
Td,
|
Td,
|
||||||
Badge,
|
Badge,
|
||||||
|
Slider,
|
||||||
|
SliderTrack,
|
||||||
|
SliderFilledTrack,
|
||||||
|
SliderThumb,
|
||||||
|
Tooltip as ChakraTooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import { RocketOutlined } from '@ant-design/icons';
|
import { ThunderboltOutlined } from '@ant-design/icons';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
// 板块状态配置
|
// 板块状态配置
|
||||||
@@ -46,7 +51,6 @@ const STATUS_CONFIG = {
|
|||||||
const generateChartOption = (themes) => {
|
const generateChartOption = (themes) => {
|
||||||
if (!themes || themes.length === 0) return {};
|
if (!themes || themes.length === 0) return {};
|
||||||
|
|
||||||
// 按状态分组
|
|
||||||
const groupedData = {
|
const groupedData = {
|
||||||
主升: [],
|
主升: [],
|
||||||
退潮: [],
|
退潮: [],
|
||||||
@@ -66,7 +70,6 @@ const generateChartOption = (themes) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建系列
|
|
||||||
const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({
|
const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({
|
||||||
name: config.name,
|
name: config.name,
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
@@ -155,32 +158,32 @@ const generateChartOption = (themes) => {
|
|||||||
left: '10%',
|
left: '10%',
|
||||||
right: '8%',
|
right: '8%',
|
||||||
top: '12%',
|
top: '12%',
|
||||||
bottom: '15%',
|
bottom: '8%',
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: '辨识度(最高板)',
|
name: '辨识度(最高板)',
|
||||||
nameLocation: 'middle',
|
nameLocation: 'middle',
|
||||||
nameGap: 30,
|
nameGap: 25,
|
||||||
nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 },
|
nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 },
|
||||||
min: 0,
|
min: 0,
|
||||||
max: maxX,
|
max: maxX,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
|
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)' } },
|
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } },
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: '板块热度(家数)',
|
name: '热度(家数)',
|
||||||
nameLocation: 'middle',
|
nameLocation: 'middle',
|
||||||
nameGap: 40,
|
nameGap: 35,
|
||||||
nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 },
|
nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 },
|
||||||
min: 0,
|
min: 0,
|
||||||
max: maxY,
|
max: maxY,
|
||||||
axisLine: { show: false },
|
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)' } },
|
splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } },
|
||||||
},
|
},
|
||||||
series,
|
series,
|
||||||
@@ -192,23 +195,29 @@ const generateChartOption = (themes) => {
|
|||||||
*/
|
*/
|
||||||
const ThemeCometChart = ({ onThemeSelect }) => {
|
const ThemeCometChart = ({ onThemeSelect }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [data, setData] = useState({ themes: [], latestDate: '' });
|
const [data, setData] = useState({ themes: [], currentDate: '', availableDates: [] });
|
||||||
const [selectedTheme, setSelectedTheme] = useState(null);
|
const [selectedTheme, setSelectedTheme] = useState(null);
|
||||||
|
const [sliderIndex, setSliderIndex] = useState(0);
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
// 加载指定日期的数据
|
||||||
const loadData = async () => {
|
const loadData = useCallback(async (dateStr = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const apiBase = getApiBase();
|
const apiBase = getApiBase();
|
||||||
const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`);
|
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();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setData({
|
setData({
|
||||||
themes: result.data.themes || [],
|
themes: result.data.themes || [],
|
||||||
latestDate: result.data.latestDate || '',
|
currentDate: result.data.currentDate || '',
|
||||||
|
availableDates: result.data.availableDates || [],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || '加载失败');
|
throw new Error(result.error || '加载失败');
|
||||||
@@ -219,10 +228,24 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
loadData();
|
|
||||||
}, [toast]);
|
}, [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 chartOption = useMemo(() => generateChartOption(data.themes), [data.themes]);
|
||||||
|
|
||||||
const handleChartClick = useCallback(
|
const handleChartClick = useCallback(
|
||||||
@@ -240,7 +263,10 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
|
|
||||||
const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]);
|
const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]);
|
||||||
|
|
||||||
if (loading) {
|
// 当前滑动条对应的日期
|
||||||
|
const currentSliderDate = data.availableDates?.[sliderIndex]?.formatted || data.currentDate;
|
||||||
|
|
||||||
|
if (loading && data.themes.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Center h="300px">
|
<Center h="300px">
|
||||||
<VStack spacing={4}>
|
<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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg="linear-gradient(135deg, rgba(15,15,30,0.6) 0%, rgba(25,25,50,0.6) 100%)"
|
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}>
|
<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)">
|
<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>
|
</Box>
|
||||||
<VStack align="start" spacing={0}>
|
<VStack align="start" spacing={0} flex={1}>
|
||||||
|
<HStack>
|
||||||
<Text fontSize="md" fontWeight="bold" color="#FFD700">
|
<Text fontSize="md" fontWeight="bold" color="#FFD700">
|
||||||
AI 舆情 · 时空决策驾驶舱
|
连板情绪监测
|
||||||
</Text>
|
</Text>
|
||||||
|
{loading && <Spinner size="xs" color="yellow.400" />}
|
||||||
|
</HStack>
|
||||||
<Text fontSize="xs" color="whiteAlpha.500">
|
<Text fontSize="xs" color="whiteAlpha.500">
|
||||||
题材流星图 · {data.latestDate}
|
{data.currentDate}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 图表区域 */}
|
{/* 图表区域 */}
|
||||||
<Box h="calc(100% - 60px)">
|
<Box h="calc(100% - 100px)">
|
||||||
|
{data.themes.length > 0 ? (
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={chartOption}
|
option={chartOption}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
onEvents={onChartEvents}
|
onEvents={onChartEvents}
|
||||||
opts={{ renderer: 'canvas' }}
|
opts={{ renderer: 'canvas' }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Center h="100%">
|
||||||
|
<Text color="whiteAlpha.500">暂无数据</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
</Box>
|
</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>
|
<Modal isOpen={isOpen} onClose={onClose} size="xl" isCentered>
|
||||||
<ModalOverlay bg="blackAlpha.700" />
|
<ModalOverlay bg="blackAlpha.700" />
|
||||||
@@ -304,7 +377,6 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
<ModalBody pb={6} overflowY="auto">
|
<ModalBody pb={6} overflowY="auto">
|
||||||
{selectedTheme && (
|
{selectedTheme && (
|
||||||
<VStack spacing={4} align="stretch">
|
<VStack spacing={4} align="stretch">
|
||||||
{/* 趋势表格 */}
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>历史数据</Text>
|
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>历史数据</Text>
|
||||||
<Table size="sm" variant="simple">
|
<Table size="sm" variant="simple">
|
||||||
@@ -339,10 +411,9 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 今日涨停股票 */}
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>
|
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>
|
||||||
今日涨停股票({selectedTheme.stocks?.length || 0}只)
|
涨停股票({selectedTheme.stocks?.length || 0}只)
|
||||||
</Text>
|
</Text>
|
||||||
<Box maxH="200px" overflowY="auto">
|
<Box maxH="200px" overflowY="auto">
|
||||||
<Table size="sm" variant="simple">
|
<Table size="sm" variant="simple">
|
||||||
@@ -370,7 +441,6 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 匹配的板块 */}
|
|
||||||
{selectedTheme.matchedSectors?.length > 0 && (
|
{selectedTheme.matchedSectors?.length > 0 && (
|
||||||
<Box>
|
<Box>
|
||||||
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>匹配板块</Text>
|
<Text color="whiteAlpha.700" fontSize="sm" mb={2}>匹配板块</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user