feat(WatchSidebar): 恢复评论模块,添加 Tab 切换
- 在关注事件面板添加"我的评论" Tab - 新增 MyCommentsTab 组件显示用户评论 - 评论显示:内容、关联事件、点赞/回复数、时间 - 更新类型定义支持评论数据传递
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 WatchlistPanel } from './WatchlistPanel';
|
||||||
export { default as FollowingEventsPanel } from './FollowingEventsPanel';
|
export { default as FollowingEventsPanel } from './FollowingEventsPanel';
|
||||||
|
export { default as MyCommentsTab } from './MyCommentsTab';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user