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

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

View File

@@ -1,24 +1,29 @@
// 关注事件面板 - 紧凑版
import React from 'react';
import { Box, Text, VStack, HStack, Icon } from '@chakra-ui/react';
import { Star, Plus, Users } from 'lucide-react';
// 关注事件面板 - 支持 Tab 切换(关注事件 / 我的评论)
import React, { useState } from 'react';
import { Box, Text, VStack, HStack, Icon, Button, ButtonGroup } from '@chakra-ui/react';
import { Star, Plus, Users, MessageSquare } from 'lucide-react';
import MyCommentsTab from './MyCommentsTab';
const TAB_EVENTS = 'events';
const TAB_COMMENTS = 'comments';
const FollowingEventsPanel = ({
events = [],
eventComments = [],
onEventClick,
onCommentClick,
onAddEvent,
}) => {
const [activeTab, setActiveTab] = useState(TAB_EVENTS);
return (
<Box>
{/* 标题 */}
{/* 标题栏 + Tab 切换 */}
<HStack justify="space-between" mb={2}>
<HStack spacing={1}>
<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>
<Text fontSize="xs" color="rgba(255, 255, 255, 0.5)">
({events.length})
事件动态
</Text>
</HStack>
<Icon
@@ -31,80 +36,138 @@ const FollowingEventsPanel = ({
/>
</HStack>
{/* 事件列表 */}
<VStack spacing={1.5} align="stretch">
{events.length === 0 ? (
<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>
) : (
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)';
{/* Tab 切换按钮 */}
<ButtonGroup size="xs" isAttached variant="outline" mb={2} w="100%">
<Button
flex={1}
fontSize="10px"
h="24px"
leftIcon={<Icon as={Star} boxSize={2.5} />}
bg={activeTab === TAB_EVENTS ? 'rgba(212, 175, 55, 0.15)' : 'transparent'}
color={activeTab === TAB_EVENTS ? 'rgba(212, 175, 55, 0.9)' : 'rgba(255, 255, 255, 0.5)'}
borderColor={activeTab === TAB_EVENTS ? '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_EVENTS)}
>
关注 ({events.length})
</Button>
<Button
flex={1}
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 (
<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>
{/* Tab 内容 */}
{activeTab === TAB_EVENTS ? (
<EventsTabContent
events={events}
onEventClick={onEventClick}
onAddEvent={onAddEvent}
/>
) : (
<MyCommentsTab
comments={eventComments}
onCommentClick={onCommentClick}
/>
)}
</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;

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 FollowingEventsPanel } from './FollowingEventsPanel';
export { default as MyCommentsTab } from './MyCommentsTab';

View File

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