feat(WatchSidebar): 恢复评论模块,添加 Tab 切换

- 在关注事件面板添加"我的评论" Tab
 - 新增 MyCommentsTab 组件显示用户评论
 - 评论显示:内容、关联事件、点赞/回复数、时间
 - 更新类型定义支持评论数据传递
This commit is contained in:
zdl
2025-12-23 10:05:45 +08:00
parent 30b831e880
commit 93928f4ee7
6 changed files with 285 additions and 82 deletions

View File

@@ -203,12 +203,18 @@ export interface WatchSidebarProps {
/** 关注的事件列表 */ /** 关注的事件列表 */
followingEvents: FollowingEvent[]; followingEvents: FollowingEvent[];
/** 用户评论列表 */
eventComments?: EventComment[];
/** 点击股票回调 */ /** 点击股票回调 */
onStockClick?: (stock: WatchlistItem) => void; onStockClick?: (stock: WatchlistItem) => void;
/** 点击事件回调 */ /** 点击事件回调 */
onEventClick?: (event: FollowingEvent) => void; onEventClick?: (event: FollowingEvent) => void;
/** 点击评论回调 */
onCommentClick?: (comment: EventComment) => void;
/** 添加股票回调 */ /** 添加股票回调 */
onAddStock?: () => void; onAddStock?: () => void;
@@ -240,9 +246,15 @@ export interface FollowingEventsPanelProps {
/** 事件列表 */ /** 事件列表 */
events: FollowingEvent[]; events: FollowingEvent[];
/** 用户评论列表 */
eventComments?: EventComment[];
/** 点击事件回调 */ /** 点击事件回调 */
onEventClick?: (event: FollowingEvent) => void; onEventClick?: (event: FollowingEvent) => void;
/** 点击评论回调 */
onCommentClick?: (comment: EventComment) => void;
/** 添加事件回调 */ /** 添加事件回调 */
onAddEvent?: () => void; onAddEvent?: () => void;
} }

View File

@@ -246,7 +246,7 @@ const CenterDashboard: React.FC = () => {
{/* 右侧固定宽度侧边栏 */} {/* 右侧固定宽度侧边栏 */}
<Box <Box
w={{ base: '100%', md: '200px' }} w={{ base: '100%', md: '300px' }}
flexShrink={0} flexShrink={0}
display={{ base: 'none', md: 'block' }} display={{ base: 'none', md: 'block' }}
position="sticky" position="sticky"
@@ -267,8 +267,10 @@ const CenterDashboard: React.FC = () => {
watchlist={watchlist} watchlist={watchlist}
realtimeQuotes={realtimeQuotes} realtimeQuotes={realtimeQuotes}
followingEvents={followingEvents} followingEvents={followingEvents}
eventComments={eventComments}
onStockClick={(stock: WatchlistItem) => navigate(`/company/${stock.stock_code}`)} onStockClick={(stock: WatchlistItem) => navigate(`/company/${stock.stock_code}`)}
onEventClick={(event: FollowingEvent) => navigate(getEventDetailUrl(event.id))} onEventClick={(event: FollowingEvent) => navigate(getEventDetailUrl(event.id))}
onCommentClick={(comment: EventComment) => navigate(getEventDetailUrl(comment.event_id))}
onAddStock={() => navigate('/stocks')} onAddStock={() => navigate('/stocks')}
onAddEvent={() => navigate('/community')} onAddEvent={() => navigate('/community')}
/> />

View File

@@ -1,24 +1,29 @@
// 关注事件面板 - 紧凑版 // 关注事件面板 - 支持 Tab 切换(关注事件 / 我的评论)
import React from 'react'; import React, { useState } from 'react';
import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react'; import { Box, Text, VStack, HStack, Icon, Button, ButtonGroup } from '@chakra-ui/react';
import { Star, Plus, Users } from 'lucide-react'; import { Star, Plus, Users, MessageSquare } from 'lucide-react';
import MyCommentsTab from './MyCommentsTab';
const TAB_EVENTS = 'events';
const TAB_COMMENTS = 'comments';
const FollowingEventsPanel = ({ const FollowingEventsPanel = ({
events = [], events = [],
eventComments = [],
onEventClick, onEventClick,
onCommentClick,
onAddEvent, onAddEvent,
}) => { }) => {
const [activeTab, setActiveTab] = useState(TAB_EVENTS);
return ( return (
<Box> <Box>
{/* 标题 */} {/* 标题栏 + Tab 切换 */}
<HStack justify="space-between" mb={2}> <HStack justify="space-between" mb={2}>
<HStack spacing={1}> <HStack spacing={1}>
<Icon as={Star} boxSize={3.5} color="rgba(234, 179, 8, 0.9)" /> <Icon as={Star} boxSize={3.5} color="rgba(234, 179, 8, 0.9)" />
<Text fontSize="xs" fontWeight="bold" color="rgba(255, 255, 255, 0.9)"> <Text fontSize="xs" fontWeight="bold" color="rgba(255, 255, 255, 0.9)">
关注事件 事件动态
</Text>
<Text fontSize="xs" color="rgba(255, 255, 255, 0.5)">
({events.length})
</Text> </Text>
</HStack> </HStack>
<Icon <Icon
@@ -31,80 +36,138 @@ const FollowingEventsPanel = ({
/> />
</HStack> </HStack>
{/* 事件列表 */} {/* Tab 切换按钮 */}
<VStack spacing={1.5} align="stretch"> <ButtonGroup size="xs" isAttached variant="outline" mb={2} w="100%">
{events.length === 0 ? ( <Button
<Box flex={1}
py={4} fontSize="10px"
textAlign="center" h="24px"
cursor="pointer" leftIcon={<Icon as={Star} boxSize={2.5} />}
onClick={onAddEvent} bg={activeTab === TAB_EVENTS ? 'rgba(212, 175, 55, 0.15)' : 'transparent'}
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} color={activeTab === TAB_EVENTS ? 'rgba(212, 175, 55, 0.9)' : 'rgba(255, 255, 255, 0.5)'}
borderRadius="md" borderColor={activeTab === TAB_EVENTS ? 'rgba(212, 175, 55, 0.4)' : 'rgba(255, 255, 255, 0.1)'}
> _hover={{
<Icon as={Star} boxSize={6} color="rgba(255, 255, 255, 0.2)" mb={1} /> bg: 'rgba(212, 175, 55, 0.1)',
<Text fontSize="xs" color="rgba(255, 255, 255, 0.4)"> color: 'rgba(212, 175, 55, 0.9)',
关注事件 }}
</Text> onClick={() => setActiveTab(TAB_EVENTS)}
</Box> >
) : ( 关注 ({events.length})
events.slice(0, 6).map((event) => { </Button>
const avgChg = event.related_avg_chg; <Button
const isUp = avgChg > 0; flex={1}
const changeColor = isUp ? '#EF4444' : avgChg < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)'; fontSize="10px"
h="24px"
leftIcon={<Icon as={MessageSquare} boxSize={2.5} />}
bg={activeTab === TAB_COMMENTS ? 'rgba(212, 175, 55, 0.15)' : 'transparent'}
color={activeTab === TAB_COMMENTS ? 'rgba(212, 175, 55, 0.9)' : 'rgba(255, 255, 255, 0.5)'}
borderColor={activeTab === TAB_COMMENTS ? 'rgba(212, 175, 55, 0.4)' : 'rgba(255, 255, 255, 0.1)'}
_hover={{
bg: 'rgba(212, 175, 55, 0.1)',
color: 'rgba(212, 175, 55, 0.9)',
}}
onClick={() => setActiveTab(TAB_COMMENTS)}
>
评论 ({eventComments.length})
</Button>
</ButtonGroup>
return ( {/* Tab 内容 */}
<Box {activeTab === TAB_EVENTS ? (
key={event.id} <EventsTabContent
py={2} events={events}
px={2} onEventClick={onEventClick}
cursor="pointer" onAddEvent={onAddEvent}
borderRadius="md" />
bg="rgba(37, 37, 64, 0.3)" ) : (
_hover={{ bg: 'rgba(37, 37, 64, 0.6)' }} <MyCommentsTab
onClick={() => onEventClick?.(event)} comments={eventComments}
> onCommentClick={onCommentClick}
<Text />
fontSize="xs" )}
fontWeight="medium"
color="rgba(255, 255, 255, 0.9)"
noOfLines={2}
mb={1}
lineHeight="1.4"
>
{event.title}
</Text>
<HStack justify="space-between" fontSize="10px">
<HStack spacing={1} color="rgba(255, 255, 255, 0.4)">
<Icon as={Users} boxSize={2.5} />
<Text>{event.follower_count || 0}</Text>
</HStack>
{avgChg !== undefined && avgChg !== null && (
<Text color={changeColor} fontWeight="medium">
{isUp ? '+' : ''}{Number(avgChg).toFixed(2)}%
</Text>
)}
</HStack>
</Box>
);
})
)}
{events.length > 6 && (
<Text
fontSize="xs"
color="rgba(212, 175, 55, 0.7)"
textAlign="center"
cursor="pointer"
py={1}
_hover={{ color: 'rgba(212, 175, 55, 0.9)' }}
onClick={onAddEvent}
>
查看全部 ({events.length})
</Text>
)}
</VStack>
</Box> </Box>
); );
}; };
/**
* 关注事件 Tab 内容
*/
const EventsTabContent = ({ events, onEventClick, onAddEvent }) => {
if (events.length === 0) {
return (
<Box
py={4}
textAlign="center"
cursor="pointer"
onClick={onAddEvent}
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
borderRadius="md"
>
<Icon as={Star} boxSize={6} color="rgba(255, 255, 255, 0.2)" mb={1} />
<Text fontSize="xs" color="rgba(255, 255, 255, 0.4)">
关注事件
</Text>
</Box>
);
}
return (
<VStack spacing={1.5} align="stretch">
{events.slice(0, 6).map((event) => {
const avgChg = event.related_avg_chg;
const isUp = avgChg > 0;
const changeColor = isUp ? '#EF4444' : avgChg < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)';
return (
<Box
key={event.id}
py={2}
px={2}
cursor="pointer"
borderRadius="md"
bg="rgba(37, 37, 64, 0.3)"
_hover={{ bg: 'rgba(37, 37, 64, 0.6)' }}
onClick={() => onEventClick?.(event)}
>
<Text
fontSize="xs"
fontWeight="medium"
color="rgba(255, 255, 255, 0.9)"
noOfLines={2}
mb={1}
lineHeight="1.4"
>
{event.title}
</Text>
<HStack justify="space-between" fontSize="10px">
<HStack spacing={1} color="rgba(255, 255, 255, 0.4)">
<Icon as={Users} boxSize={2.5} />
<Text>{event.follower_count || 0}</Text>
</HStack>
{avgChg !== undefined && avgChg !== null && (
<Text color={changeColor} fontWeight="medium">
{isUp ? '+' : ''}{Number(avgChg).toFixed(2)}%
</Text>
)}
</HStack>
</Box>
);
})}
{events.length > 6 && (
<Text
fontSize="xs"
color="rgba(212, 175, 55, 0.7)"
textAlign="center"
cursor="pointer"
py={1}
_hover={{ color: 'rgba(212, 175, 55, 0.9)' }}
onClick={onAddEvent}
>
查看全部 ({events.length})
</Text>
)}
</VStack>
);
};
export default FollowingEventsPanel; export default FollowingEventsPanel;

View File

@@ -0,0 +1,121 @@
// 我的评论 Tab 组件
import React from 'react';
import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react';
import { MessageSquare, ThumbsUp, MessageCircle } from 'lucide-react';
/**
* 格式化时间为相对时间
*/
const formatRelativeTime = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 30) return `${days}天前`;
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
};
/**
* 截断文本
*/
const truncateText = (text, maxLength = 50) => {
if (!text) return '';
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
};
const MyCommentsTab = ({
comments = [],
onCommentClick,
maxDisplay = 5,
}) => {
const displayComments = comments.slice(0, maxDisplay);
if (comments.length === 0) {
return (
<Box py={4} textAlign="center">
<Icon as={MessageSquare} boxSize={6} color="rgba(255, 255, 255, 0.2)" mb={1} />
<Text fontSize="xs" color="rgba(255, 255, 255, 0.4)">
暂无评论
</Text>
</Box>
);
}
return (
<VStack spacing={1.5} align="stretch">
{displayComments.map((comment) => (
<Box
key={comment.id}
py={2}
px={2}
cursor="pointer"
borderRadius="md"
bg="rgba(37, 37, 64, 0.3)"
_hover={{ bg: 'rgba(37, 37, 64, 0.6)' }}
onClick={() => onCommentClick?.(comment)}
>
{/* 评论内容 */}
<Text
fontSize="xs"
color="rgba(255, 255, 255, 0.85)"
noOfLines={2}
mb={1}
lineHeight="1.4"
>
{truncateText(comment.content, 60)}
</Text>
{/* 关联事件 */}
{comment.event_title && (
<Text
fontSize="10px"
color="rgba(212, 175, 55, 0.7)"
noOfLines={1}
mb={1}
>
📌 {truncateText(comment.event_title, 30)}
</Text>
)}
{/* 底部信息:点赞、回复、时间 */}
<HStack justify="space-between" fontSize="10px" color="rgba(255, 255, 255, 0.4)">
<HStack spacing={2}>
<HStack spacing={0.5}>
<Icon as={ThumbsUp} boxSize={2.5} />
<Text>{comment.like_count || 0}</Text>
</HStack>
<HStack spacing={0.5}>
<Icon as={MessageCircle} boxSize={2.5} />
<Text>{comment.reply_count || 0}</Text>
</HStack>
</HStack>
<Text>{formatRelativeTime(comment.created_at)}</Text>
</HStack>
</Box>
))}
{/* 查看更多 */}
{comments.length > maxDisplay && (
<Text
fontSize="xs"
color="rgba(212, 175, 55, 0.7)"
textAlign="center"
cursor="pointer"
py={1}
_hover={{ color: 'rgba(212, 175, 55, 0.9)' }}
>
查看全部 ({comments.length})
</Text>
)}
</VStack>
);
};
export default MyCommentsTab;

View File

@@ -1,3 +1,4 @@
// 侧边栏子组件导出 // 侧边栏子组件导出
export { default as WatchlistPanel } from './WatchlistPanel'; export { default as WatchlistPanel } from './WatchlistPanel';
export { default as FollowingEventsPanel } from './FollowingEventsPanel'; export { default as FollowingEventsPanel } from './FollowingEventsPanel';
export { default as MyCommentsTab } from './MyCommentsTab';

View File

@@ -8,8 +8,10 @@ const WatchSidebar = ({
watchlist = [], watchlist = [],
realtimeQuotes = {}, realtimeQuotes = {},
followingEvents = [], followingEvents = [],
eventComments = [],
onStockClick, onStockClick,
onEventClick, onEventClick,
onCommentClick,
onAddStock, onAddStock,
onAddEvent, onAddEvent,
}) => { }) => {
@@ -30,7 +32,7 @@ const WatchSidebar = ({
/> />
</GlassCard> </GlassCard>
{/* 关注事件 - 独立模块 */} {/* 关注事件 + 我的评论 - 独立模块 */}
<GlassCard <GlassCard
variant="transparent" variant="transparent"
rounded="xl" rounded="xl"
@@ -39,7 +41,9 @@ const WatchSidebar = ({
> >
<FollowingEventsPanel <FollowingEventsPanel
events={followingEvents} events={followingEvents}
eventComments={eventComments}
onEventClick={onEventClick} onEventClick={onEventClick}
onCommentClick={onCommentClick}
onAddEvent={onAddEvent} onAddEvent={onAddEvent}
/> />
</GlassCard> </GlassCard>