community增加事件详情

This commit is contained in:
2026-01-08 18:25:49 +08:00
parent cb4871416a
commit d088bcbd12
2 changed files with 159 additions and 52 deletions

76
app.py
View File

@@ -11004,9 +11004,12 @@ def get_events_effectiveness_stats():
按交易日统计事件数据,展示事件预测的有效性
交易日定义上一交易日15:00到当前交易日15:00
例如周一15:00到周二15:00为"周二交易日"的事件
参数:
date: 指定日期YYYY-MM-DD格式可选默认今天
days: 统计天数默认7天
days: 统计天数默认7天days=1表示当前交易日
返回:
{
@@ -11041,21 +11044,78 @@ def get_events_effectiveness_stats():
days = request.args.get('days', 7, type=int)
days = min(max(days, 1), 30) # 限制1-30天
# 确定查询日期范围
# 确定基准时间
if date_str:
try:
end_date = datetime.strptime(date_str, '%Y-%m-%d')
base_date = datetime.strptime(date_str, '%Y-%m-%d')
except ValueError:
end_date = datetime.now()
base_date = datetime.now()
else:
end_date = datetime.now()
base_date = datetime.now()
start_date = end_date - timedelta(days=days)
# 使用交易日15:00作为分界点计算时间范围
# 当前交易日的结束时间当天15:00如果当前时间<15:00或下一交易日15:00如果当前时间>=15:00
current_time = base_date.time() if isinstance(base_date, datetime) else dt_time(12, 0)
current_date = base_date.date() if isinstance(base_date, datetime) else base_date
# 确保交易日数据已加载
if not trading_days:
load_trading_days()
# 判断当前是否是交易日以及是否收盘后
is_trading = current_date in trading_days_set
is_after_close = current_time >= dt_time(15, 0)
# 确定当前交易日
if is_trading:
if is_after_close:
# 交易日收盘后,当前交易日就是今天
current_trading_day = current_date
else:
# 交易日盘中,当前交易日是今天
current_trading_day = current_date
else:
# 非交易日,找最近的上一个交易日
current_trading_day = None
for td in reversed(trading_days):
if td <= current_date:
current_trading_day = td
break
if not current_trading_day:
current_trading_day = current_date
# 计算时间范围每个交易日从上一交易日15:00到当天15:00
# 对于 days=1当前交易日范围是上一交易日15:00 到 当前交易日15:00
# 找到往前 days 个交易日
try:
current_idx = trading_days.index(current_trading_day)
start_trading_day_idx = max(0, current_idx - days + 1)
start_trading_day = trading_days[start_trading_day_idx]
# 找 start_trading_day 的前一个交易日
if start_trading_day_idx > 0:
prev_start_day = trading_days[start_trading_day_idx - 1]
else:
prev_start_day = start_trading_day - timedelta(days=1)
except ValueError:
# current_trading_day 不在列表中,使用简单计算
start_trading_day = current_trading_day - timedelta(days=days)
prev_start_day = start_trading_day - timedelta(days=1)
# 查询时间范围:从 prev_start_day 15:00 到 current_trading_day 15:00
start_datetime = datetime.combine(prev_start_day, dt_time(15, 0))
end_datetime = datetime.combine(current_trading_day, dt_time(15, 0))
# 如果当前时间还没到15:00结束时间用当前时间
if is_trading and not is_after_close:
end_datetime = base_date if isinstance(base_date, datetime) else datetime.combine(current_date, dt_time(23, 59))
# 查询事件数据
events_query = db.session.query(Event).filter(
Event.created_at >= start_date,
Event.created_at <= end_date + timedelta(days=1),
Event.created_at >= start_datetime,
Event.created_at <= end_datetime,
Event.status == 'active'
).order_by(Event.created_at.desc()).all()

View File

@@ -24,6 +24,17 @@ import {
} from '@ant-design/icons';
import { getApiBase } from '@utils/apiConfig';
/**
* 生成事件详情页 URL
* @param {number} eventId - 事件ID
* @returns {string} 事件详情页 URL
*/
const getEventDetailUrl = (eventId) => {
// 使用 base64 编码 ID格式ev-{id} -> base64
const encodedId = btoa(`ev-${eventId}`);
return `/event-detail?id=${encodedId}`;
};
/**
* 格式化涨跌幅
*/
@@ -99,48 +110,61 @@ const CompactStatCard = ({ label, value, icon, color = '#FFD700', subText, progr
);
/**
* TOP事件列表项
* TOP事件列表项 - 支持点击跳转
*/
const TopEventItem = ({ event, rank }) => (
<HStack
spacing={2}
py={1.5}
px={2}
bg="rgba(0,0,0,0.2)"
borderRadius="md"
_hover={{ bg: 'rgba(255,215,0,0.08)' }}
transition="all 0.15s"
>
<Badge
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
fontSize="2xs"
px={1.5}
borderRadius="full"
minW="18px"
textAlign="center"
const TopEventItem = ({ event, rank }) => {
const handleClick = () => {
if (event.id) {
// 在新标签页打开事件详情
window.open(getEventDetailUrl(event.id), '_blank');
}
};
return (
<HStack
spacing={2}
py={1.5}
px={2}
bg="rgba(0,0,0,0.2)"
borderRadius="md"
_hover={{ bg: 'rgba(255,215,0,0.12)', cursor: 'pointer' }}
transition="all 0.15s"
onClick={handleClick}
role="button"
tabIndex={0}
onKeyPress={(e) => e.key === 'Enter' && handleClick()}
>
{rank}
</Badge>
<Tooltip label={event.title} placement="top" hasArrow>
<Badge
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
fontSize="2xs"
px={1.5}
borderRadius="full"
minW="18px"
textAlign="center"
>
{rank}
</Badge>
<Tooltip label={event.title} placement="top" hasArrow>
<Text
fontSize="xs"
color="gray.300"
flex="1"
noOfLines={1}
_hover={{ color: '#FFD700' }}
>
{event.title}
</Text>
</Tooltip>
<Text
fontSize="xs"
color="gray.300"
flex="1"
noOfLines={1}
cursor="default"
fontWeight="bold"
color={getChgColor(event.maxChg)}
>
{event.title}
{formatChg(event.maxChg)}
</Text>
</Tooltip>
<Text
fontSize="xs"
fontWeight="bold"
color={getChgColor(event.maxChg)}
>
{formatChg(event.maxChg)}
</Text>
</HStack>
);
</HStack>
);
};
const EventDailyStats = () => {
const [loading, setLoading] = useState(true);
@@ -215,9 +239,7 @@ const EventDailyStats = () => {
);
}
const { summary, topPerformers = [], dailyStats = [] } = stats;
// 获取当日TOP事件
const todayTopEvents = dailyStats[0]?.topEvents || topPerformers.slice(0, 3);
const { summary, topPerformers = [] } = stats;
return (
<Box
@@ -329,19 +351,44 @@ const EventDailyStats = () => {
{/* 分割线 */}
<Box h="1px" bg="rgba(255,215,0,0.1)" />
{/* TOP表现事件 */}
<Box>
{/* TOP表现事件 - 显示 TOP10 */}
<Box flex="1" overflow="hidden">
<HStack spacing={1.5} mb={2}>
<TrophyOutlined style={{ color: '#FFD700', fontSize: '12px' }} />
<Text fontSize="xs" fontWeight="bold" color="gray.400">
今日 TOP 表现
</Text>
<Text fontSize="2xs" color="gray.600">
(点击查看详情)
</Text>
</HStack>
<VStack spacing={1.5} align="stretch">
{todayTopEvents.slice(0, 3).map((event, idx) => (
<VStack
spacing={1}
align="stretch"
maxH="220px"
overflowY="auto"
pr={1}
css={{
'&::-webkit-scrollbar': {
width: '4px',
},
'&::-webkit-scrollbar-track': {
background: 'rgba(255,255,255,0.05)',
borderRadius: '2px',
},
'&::-webkit-scrollbar-thumb': {
background: 'rgba(255,215,0,0.3)',
borderRadius: '2px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: 'rgba(255,215,0,0.5)',
},
}}
>
{topPerformers.slice(0, 10).map((event, idx) => (
<TopEventItem key={event.id || idx} event={event} rank={idx + 1} />
))}
{todayTopEvents.length === 0 && (
{topPerformers.length === 0 && (
<Text fontSize="xs" color="gray.600" textAlign="center" py={2}>
暂无数据
</Text>