事件标记线bug修复
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user