community增加事件详情
This commit is contained in:
76
app.py
76
app.py
@@ -11004,9 +11004,12 @@ def get_events_effectiveness_stats():
|
|||||||
|
|
||||||
按交易日统计事件数据,展示事件预测的有效性
|
按交易日统计事件数据,展示事件预测的有效性
|
||||||
|
|
||||||
|
交易日定义:上一交易日15:00到当前交易日15:00
|
||||||
|
例如:周一15:00到周二15:00为"周二交易日"的事件
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
date: 指定日期(YYYY-MM-DD格式,可选,默认今天)
|
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 = request.args.get('days', 7, type=int)
|
||||||
days = min(max(days, 1), 30) # 限制1-30天
|
days = min(max(days, 1), 30) # 限制1-30天
|
||||||
|
|
||||||
# 确定查询日期范围
|
# 确定基准时间
|
||||||
if date_str:
|
if date_str:
|
||||||
try:
|
try:
|
||||||
end_date = datetime.strptime(date_str, '%Y-%m-%d')
|
base_date = datetime.strptime(date_str, '%Y-%m-%d')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
end_date = datetime.now()
|
base_date = datetime.now()
|
||||||
else:
|
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(
|
events_query = db.session.query(Event).filter(
|
||||||
Event.created_at >= start_date,
|
Event.created_at >= start_datetime,
|
||||||
Event.created_at <= end_date + timedelta(days=1),
|
Event.created_at <= end_datetime,
|
||||||
Event.status == 'active'
|
Event.status == 'active'
|
||||||
).order_by(Event.created_at.desc()).all()
|
).order_by(Event.created_at.desc()).all()
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
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 }) => (
|
const TopEventItem = ({ event, rank }) => {
|
||||||
<HStack
|
const handleClick = () => {
|
||||||
spacing={2}
|
if (event.id) {
|
||||||
py={1.5}
|
// 在新标签页打开事件详情
|
||||||
px={2}
|
window.open(getEventDetailUrl(event.id), '_blank');
|
||||||
bg="rgba(0,0,0,0.2)"
|
}
|
||||||
borderRadius="md"
|
};
|
||||||
_hover={{ bg: 'rgba(255,215,0,0.08)' }}
|
|
||||||
transition="all 0.15s"
|
return (
|
||||||
>
|
<HStack
|
||||||
<Badge
|
spacing={2}
|
||||||
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
|
py={1.5}
|
||||||
fontSize="2xs"
|
px={2}
|
||||||
px={1.5}
|
bg="rgba(0,0,0,0.2)"
|
||||||
borderRadius="full"
|
borderRadius="md"
|
||||||
minW="18px"
|
_hover={{ bg: 'rgba(255,215,0,0.12)', cursor: 'pointer' }}
|
||||||
textAlign="center"
|
transition="all 0.15s"
|
||||||
|
onClick={handleClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleClick()}
|
||||||
>
|
>
|
||||||
{rank}
|
<Badge
|
||||||
</Badge>
|
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
|
||||||
<Tooltip label={event.title} placement="top" hasArrow>
|
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
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
color="gray.300"
|
fontWeight="bold"
|
||||||
flex="1"
|
color={getChgColor(event.maxChg)}
|
||||||
noOfLines={1}
|
|
||||||
cursor="default"
|
|
||||||
>
|
>
|
||||||
{event.title}
|
{formatChg(event.maxChg)}
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</HStack>
|
||||||
<Text
|
);
|
||||||
fontSize="xs"
|
};
|
||||||
fontWeight="bold"
|
|
||||||
color={getChgColor(event.maxChg)}
|
|
||||||
>
|
|
||||||
{formatChg(event.maxChg)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
|
|
||||||
const EventDailyStats = () => {
|
const EventDailyStats = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -215,9 +239,7 @@ const EventDailyStats = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { summary, topPerformers = [], dailyStats = [] } = stats;
|
const { summary, topPerformers = [] } = stats;
|
||||||
// 获取当日TOP事件
|
|
||||||
const todayTopEvents = dailyStats[0]?.topEvents || topPerformers.slice(0, 3);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -329,19 +351,44 @@ const EventDailyStats = () => {
|
|||||||
{/* 分割线 */}
|
{/* 分割线 */}
|
||||||
<Box h="1px" bg="rgba(255,215,0,0.1)" />
|
<Box h="1px" bg="rgba(255,215,0,0.1)" />
|
||||||
|
|
||||||
{/* TOP表现事件 */}
|
{/* TOP表现事件 - 显示 TOP10 */}
|
||||||
<Box>
|
<Box flex="1" overflow="hidden">
|
||||||
<HStack spacing={1.5} mb={2}>
|
<HStack spacing={1.5} mb={2}>
|
||||||
<TrophyOutlined style={{ color: '#FFD700', fontSize: '12px' }} />
|
<TrophyOutlined style={{ color: '#FFD700', fontSize: '12px' }} />
|
||||||
<Text fontSize="xs" fontWeight="bold" color="gray.400">
|
<Text fontSize="xs" fontWeight="bold" color="gray.400">
|
||||||
今日 TOP 表现
|
今日 TOP 表现
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text fontSize="2xs" color="gray.600">
|
||||||
|
(点击查看详情)
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<VStack spacing={1.5} align="stretch">
|
<VStack
|
||||||
{todayTopEvents.slice(0, 3).map((event, idx) => (
|
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} />
|
<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 fontSize="xs" color="gray.600" textAlign="center" py={2}>
|
||||||
暂无数据
|
暂无数据
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user