feat(WatchSidebar): 恢复评论模块,添加 Tab 切换
- 在关注事件面板添加"我的评论" Tab - 新增 MyCommentsTab 组件显示用户评论 - 评论显示:内容、关联事件、点赞/回复数、时间 - 更新类型定义支持评论数据传递
This commit is contained in:
@@ -203,12 +203,18 @@ export interface WatchSidebarProps {
|
||||
/** 关注的事件列表 */
|
||||
followingEvents: FollowingEvent[];
|
||||
|
||||
/** 用户评论列表 */
|
||||
eventComments?: EventComment[];
|
||||
|
||||
/** 点击股票回调 */
|
||||
onStockClick?: (stock: WatchlistItem) => void;
|
||||
|
||||
/** 点击事件回调 */
|
||||
onEventClick?: (event: FollowingEvent) => void;
|
||||
|
||||
/** 点击评论回调 */
|
||||
onCommentClick?: (comment: EventComment) => void;
|
||||
|
||||
/** 添加股票回调 */
|
||||
onAddStock?: () => void;
|
||||
|
||||
@@ -240,9 +246,15 @@ export interface FollowingEventsPanelProps {
|
||||
/** 事件列表 */
|
||||
events: FollowingEvent[];
|
||||
|
||||
/** 用户评论列表 */
|
||||
eventComments?: EventComment[];
|
||||
|
||||
/** 点击事件回调 */
|
||||
onEventClick?: (event: FollowingEvent) => void;
|
||||
|
||||
/** 点击评论回调 */
|
||||
onCommentClick?: (comment: EventComment) => void;
|
||||
|
||||
/** 添加事件回调 */
|
||||
onAddEvent?: () => void;
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,3 +1,4 @@
|
||||
// 侧边栏子组件导出
|
||||
export { default as WatchlistPanel } from './WatchlistPanel';
|
||||
export { default as FollowingEventsPanel } from './FollowingEventsPanel';
|
||||
export { default as MyCommentsTab } from './MyCommentsTab';
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user