事件标记线bug修复

This commit is contained in:
2025-12-25 13:15:57 +08:00
parent 57cd0aa8ec
commit bcafafe34c
7 changed files with 454 additions and 7 deletions

View File

@@ -888,6 +888,10 @@ export function generateMockEvents(params = {}) {
const tradingDate = new Date(createdAt);
tradingDate.setDate(tradingDate.getDate() + 1);
// 生成投票数据
const bullishCount = Math.floor(Math.random() * 100) + 5;
const bearishCount = Math.floor(Math.random() * 50) + 3;
allEvents.push({
id: i + 1,
title: generateEventTitle(industry, i),
@@ -901,6 +905,10 @@ export function generateMockEvents(params = {}) {
trading_date: tradingDate.toISOString().split('T')[0], // YYYY-MM-DD 格式
hot_score: hotScore,
view_count: Math.floor(Math.random() * 10000),
follower_count: Math.floor(Math.random() * 500) + 10,
bullish_count: bullishCount,
bearish_count: bearishCount,
user_vote: null, // 默认未投票
related_avg_chg: parseFloat(relatedAvgChg),
related_max_chg: parseFloat(relatedMaxChg),
related_week_chg: parseFloat(relatedWeekChg),
@@ -1374,6 +1382,10 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
...generateKeywords(industry, i).slice(0, 2)
];
// 生成投票数据
const bullishCount = Math.floor(Math.random() * 80) + 10;
const bearishCount = Math.floor(Math.random() * 40) + 5;
events.push({
id: `dynamic_${i + 1}`,
title: eventTitle,
@@ -1387,6 +1399,9 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
hot_score: hotScore,
view_count: Math.floor(Math.random() * 5000) + 1000, // 1000-6000 浏览量
follower_count: Math.floor(Math.random() * 500) + 50, // 50-550 关注数
bullish_count: bullishCount, // 看多数
bearish_count: bearishCount, // 看空数
user_vote: null, // 默认未投票
post_count: Math.floor(Math.random() * 100) + 10, // 10-110 帖子数
related_avg_chg: parseFloat(relatedAvgChg),
related_max_chg: parseFloat(relatedMaxChg),

View File

@@ -601,6 +601,56 @@ export const eventHandlers = [
}
}),
// 事件情绪投票(看多/看空)
http.post('/api/events/:eventId/sentiment-vote', async ({ params, request }) => {
await delay(200);
const { eventId } = params;
const numericEventId = parseInt(eventId, 10);
console.log('[Mock] 事件情绪投票, eventId:', numericEventId);
try {
const body = await request.json();
const voteType = body.vote_type; // 'bullish', 'bearish', 或 null
// 使用内存状态管理投票
// 简单模拟:根据 eventId 生成基础数据
const baseBullish = (numericEventId * 7) % 50 + 10;
const baseBearish = (numericEventId * 3) % 30 + 5;
// 根据投票类型调整计数
let bullishCount = baseBullish;
let bearishCount = baseBearish;
if (voteType === 'bullish') {
bullishCount += 1;
} else if (voteType === 'bearish') {
bearishCount += 1;
}
return HttpResponse.json({
success: true,
data: {
user_vote: voteType || null,
bullish_count: bullishCount,
bearish_count: bearishCount,
},
message: voteType ? '投票成功' : '取消投票成功'
});
} catch (error) {
console.error('[Mock] 事件情绪投票失败:', error);
return HttpResponse.json(
{
success: false,
error: '投票失败',
data: null
},
{ status: 500 }
);
}
}),
// 获取事件传导链分析数据
http.get('/api/events/:eventId/transmission', async ({ params }) => {
await delay(500);

View File

@@ -23,6 +23,7 @@ import {
ImportanceStamp,
EventTimeline,
EventFollowButton,
EventEngagement,
KeywordsCarousel,
} from './atoms';
import StockChangeIndicators from '@components/StockChangeIndicators';
@@ -38,10 +39,12 @@ import StockChangeIndicators from '@components/StockChangeIndicators';
* @param {Function} props.onEventClick - 卡片点击事件
* @param {Function} props.onTitleClick - 标题点击事件
* @param {Function} props.onToggleFollow - 切换关注事件
* @param {Function} props.onVoteChange - 投票变化回调
* @param {Object} props.timelineStyle - 时间轴样式配置
* @param {string} props.borderColor - 边框颜色
* @param {string} props.indicatorSize - 涨幅指标尺寸 ('default' | 'comfortable' | 'large')
* @param {string} props.layout - 布局模式 ('vertical' | 'four-row'),影响时间轴竖线高度
* @param {boolean} props.showEngagement - 是否显示互动指标(浏览/关注/投票)
*/
const HorizontalDynamicNewsEventCard = React.memo(({
event,
@@ -52,10 +55,12 @@ const HorizontalDynamicNewsEventCard = React.memo(({
onEventClick,
onTitleClick,
onToggleFollow,
onVoteChange,
timelineStyle,
borderColor: timelineBorderColor,
indicatorSize = 'comfortable',
layout = 'vertical',
showEngagement = true,
}) => {
const importance = getImportanceConfig(event.importance);
const { isMobile } = useDevice();
@@ -247,13 +252,29 @@ const HorizontalDynamicNewsEventCard = React.memo(({
</Box>
</Tooltip>
{/* 第二行:涨跌幅数据 */}
<StockChangeIndicators
maxChange={event.related_max_chg}
avgChange={event.related_avg_chg}
expectationScore={event.expectation_surprise_score}
size={indicatorSize}
/>
{/* 第二行:涨跌幅数据 + 互动指标 */}
<HStack justify="space-between" align="center" flexWrap="wrap" gap={1}>
<StockChangeIndicators
maxChange={event.related_max_chg}
avgChange={event.related_avg_chg}
expectationScore={event.expectation_surprise_score}
size={indicatorSize}
/>
{/* 互动指标:浏览量、关注数、看多/看空 */}
{showEngagement && (
<EventEngagement
eventId={event.id}
viewCount={event.view_count}
followerCount={event.follower_count}
bullishCount={event.bullish_count}
bearishCount={event.bearish_count}
userVote={event.user_vote}
size="xs"
onVoteChange={onVoteChange}
/>
)}
</HStack>
</VStack>
</CardBody>
</Card>

View File

@@ -0,0 +1,254 @@
// src/views/Community/components/EventCard/atoms/EventEngagement.js
// 事件互动组件 - 显示浏览量、关注量和看多/看空投票
import React, { useState, useCallback } from 'react';
import {
HStack,
Box,
Text,
Tooltip,
IconButton,
Badge,
useToast,
} from '@chakra-ui/react';
import { ViewIcon, StarIcon } from '@chakra-ui/icons';
import { TbArrowBigUp, TbArrowBigDown, TbArrowBigUpFilled, TbArrowBigDownFilled } from 'react-icons/tb';
import { getApiBase } from '@utils/apiConfig';
import { useAuth } from '@contexts/AuthContext';
/**
* 格式化数字大于1000显示为 1k, 1.2k 等)
* @param {number} num - 原始数字
* @returns {string} 格式化后的字符串
*/
const formatCount = (num) => {
if (num == null || isNaN(num)) return '0';
if (num >= 10000) return `${(num / 10000).toFixed(1)}w`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;
return String(num);
};
/**
* 事件互动组件
* @param {Object} props
* @param {number} props.eventId - 事件ID
* @param {number} props.viewCount - 浏览量
* @param {number} props.followerCount - 关注数
* @param {number} props.bullishCount - 看多数
* @param {number} props.bearishCount - 看空数
* @param {string} props.userVote - 用户当前投票状态 ('bullish' | 'bearish' | null)
* @param {string} props.size - 尺寸 ('xs' | 'sm' | 'md')
* @param {boolean} props.showVoting - 是否显示投票功能
* @param {Function} props.onVoteChange - 投票变化回调
*/
const EventEngagement = ({
eventId,
viewCount = 0,
followerCount = 0,
bullishCount = 0,
bearishCount = 0,
userVote = null,
size = 'sm',
showVoting = true,
onVoteChange,
}) => {
const toast = useToast();
const { isLoggedIn } = useAuth();
// 本地状态管理(乐观更新)
const [localVote, setLocalVote] = useState(userVote);
const [localBullish, setLocalBullish] = useState(bullishCount);
const [localBearish, setLocalBearish] = useState(bearishCount);
const [isVoting, setIsVoting] = useState(false);
// 尺寸配置
const sizeConfig = {
xs: { fontSize: '10px', iconSize: '12px', spacing: 1, btnSize: 'xs' },
sm: { fontSize: 'xs', iconSize: '14px', spacing: 1.5, btnSize: 'xs' },
md: { fontSize: 'sm', iconSize: '16px', spacing: 2, btnSize: 'sm' },
};
const config = sizeConfig[size] || sizeConfig.sm;
/**
* 处理投票
* @param {string} voteType - 'bullish' | 'bearish'
*/
const handleVote = useCallback(async (voteType) => {
if (!isLoggedIn) {
toast({
title: '请先登录',
description: '登录后才能参与投票',
status: 'warning',
duration: 2000,
isClosable: true,
});
return;
}
if (isVoting) return;
setIsVoting(true);
// 计算新的投票状态
const newVote = localVote === voteType ? null : voteType;
const oldVote = localVote;
// 乐观更新本地状态
setLocalVote(newVote);
// 更新计数
let newBullish = localBullish;
let newBearish = localBearish;
// 取消之前的投票
if (oldVote === 'bullish') newBullish--;
if (oldVote === 'bearish') newBearish--;
// 添加新投票
if (newVote === 'bullish') newBullish++;
if (newVote === 'bearish') newBearish++;
setLocalBullish(Math.max(0, newBullish));
setLocalBearish(Math.max(0, newBearish));
try {
const response = await fetch(`${getApiBase()}/api/events/${eventId}/sentiment-vote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ vote_type: newVote }),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '投票失败');
}
// 使用服务器返回的准确数据
if (data.data) {
setLocalBullish(data.data.bullish_count ?? newBullish);
setLocalBearish(data.data.bearish_count ?? newBearish);
setLocalVote(data.data.user_vote);
}
// 触发回调
onVoteChange?.({
eventId,
userVote: data.data?.user_vote,
bullishCount: data.data?.bullish_count,
bearishCount: data.data?.bearish_count,
});
} catch (error) {
console.error('投票失败:', error);
// 回滚状态
setLocalVote(oldVote);
setLocalBullish(bullishCount);
setLocalBearish(bearishCount);
toast({
title: '投票失败',
description: error.message,
status: 'error',
duration: 2000,
isClosable: true,
});
} finally {
setIsVoting(false);
}
}, [eventId, isLoggedIn, isVoting, localVote, localBullish, localBearish, bullishCount, bearishCount, toast, onVoteChange]);
// 计算看多比例(用于显示)
const totalVotes = localBullish + localBearish;
const bullishRatio = totalVotes > 0 ? Math.round((localBullish / totalVotes) * 100) : 50;
return (
<HStack spacing={config.spacing} onClick={(e) => e.stopPropagation()}>
{/* 浏览量 */}
<Tooltip label="浏览量" placement="top" hasArrow>
<HStack spacing={0.5} color="gray.400">
<ViewIcon boxSize={config.iconSize} />
<Text fontSize={config.fontSize}>{formatCount(viewCount)}</Text>
</HStack>
</Tooltip>
{/* 关注数 */}
<Tooltip label="关注数" placement="top" hasArrow>
<HStack spacing={0.5} color="gray.400">
<StarIcon boxSize={config.iconSize} />
<Text fontSize={config.fontSize}>{formatCount(followerCount)}</Text>
</HStack>
</Tooltip>
{/* 看多/看空投票 */}
{showVoting && (
<HStack spacing={0.5} ml={1}>
{/* 看多按钮 */}
<Tooltip label={localVote === 'bullish' ? '取消看多' : '看多'} placement="top" hasArrow>
<IconButton
size={config.btnSize}
variant="ghost"
colorScheme="red"
icon={localVote === 'bullish'
? <TbArrowBigUpFilled size={config.iconSize} />
: <TbArrowBigUp size={config.iconSize} />
}
onClick={() => handleVote('bullish')}
isLoading={isVoting}
aria-label="看多"
minW="auto"
h="auto"
p={0.5}
color={localVote === 'bullish' ? 'red.400' : 'gray.400'}
_hover={{ color: 'red.400', bg: 'rgba(239, 68, 68, 0.1)' }}
/>
</Tooltip>
{/* 投票比例显示 */}
{totalVotes > 0 && (
<Tooltip
label={`看多 ${localBullish} | 看空 ${localBearish}`}
placement="top"
hasArrow
>
<Badge
fontSize={config.fontSize}
px={1}
py={0}
borderRadius="sm"
bg={bullishRatio >= 50 ? 'rgba(239, 68, 68, 0.15)' : 'rgba(16, 185, 129, 0.15)'}
color={bullishRatio >= 50 ? 'red.400' : 'green.400'}
fontWeight="medium"
>
{bullishRatio}%
</Badge>
</Tooltip>
)}
{/* 看空按钮 */}
<Tooltip label={localVote === 'bearish' ? '取消看空' : '看空'} placement="top" hasArrow>
<IconButton
size={config.btnSize}
variant="ghost"
colorScheme="green"
icon={localVote === 'bearish'
? <TbArrowBigDownFilled size={config.iconSize} />
: <TbArrowBigDown size={config.iconSize} />
}
onClick={() => handleVote('bearish')}
isLoading={isVoting}
aria-label="看空"
minW="auto"
h="auto"
p={0.5}
color={localVote === 'bearish' ? 'green.400' : 'gray.400'}
_hover={{ color: 'green.400', bg: 'rgba(16, 185, 129, 0.1)' }}
/>
</Tooltip>
</HStack>
)}
</HStack>
);
};
export default EventEngagement;

View File

@@ -2,6 +2,7 @@
// 事件卡片原子组件
export { default as EventDescription } from './EventDescription';
export { default as EventEngagement } from './EventEngagement';
export { default as EventFollowButton } from './EventFollowButton';
export { default as EventHeader } from './EventHeader';
export { default as EventImportanceBadge } from './EventImportanceBadge';