feat(WatchSidebar): 增强关注事件面板功能

- FollowingEventsPanel: 添加取消关注功能 (onUnfollow)
- FollowingEventsPanel: 显示日涨跌和周涨跌两个指标
- WatchlistPanel: 优化布局和样式
- index.js: 导出 useGlobalSidebar hook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-23 11:56:53 +08:00
parent dafef2c572
commit 89ed59640e
3 changed files with 124 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
// 关注事件面板 - 支持 Tab 切换(关注事件 / 我的评论) // 关注事件面板 - 支持 Tab 切换(关注事件 / 我的评论)
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, Text, VStack, HStack, Icon, Button, ButtonGroup } from '@chakra-ui/react'; import { Box, Text, VStack, HStack, Icon, Button, ButtonGroup, Badge } from '@chakra-ui/react';
import { Star, Plus, Users, MessageSquare } from 'lucide-react'; import { Star, Plus, Users, MessageSquare } from 'lucide-react';
import MyCommentsTab from './MyCommentsTab'; import MyCommentsTab from './MyCommentsTab';
@@ -13,6 +13,7 @@ const FollowingEventsPanel = ({
onEventClick, onEventClick,
onCommentClick, onCommentClick,
onAddEvent, onAddEvent,
onUnfollow,
}) => { }) => {
const [activeTab, setActiveTab] = useState(TAB_EVENTS); const [activeTab, setActiveTab] = useState(TAB_EVENTS);
@@ -78,6 +79,7 @@ const FollowingEventsPanel = ({
events={events} events={events}
onEventClick={onEventClick} onEventClick={onEventClick}
onAddEvent={onAddEvent} onAddEvent={onAddEvent}
onUnfollow={onUnfollow}
/> />
) : ( ) : (
<MyCommentsTab <MyCommentsTab
@@ -89,10 +91,36 @@ const FollowingEventsPanel = ({
); );
}; };
/**
* 格式化涨跌幅
*/
const formatChange = (value) => {
if (value === undefined || value === null) return null;
const num = Number(value);
const isUp = num > 0;
return {
text: `${isUp ? '+' : ''}${num.toFixed(2)}%`,
color: isUp ? '#EF4444' : num < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)',
};
};
/** /**
* 关注事件 Tab 内容 * 关注事件 Tab 内容
*/ */
const EventsTabContent = ({ events, onEventClick, onAddEvent }) => { const EventsTabContent = ({ events, onEventClick, onAddEvent, onUnfollow }) => {
const [unfollowingId, setUnfollowingId] = useState(null);
const handleUnfollow = async (e, eventId) => {
e.stopPropagation();
if (unfollowingId) return;
setUnfollowingId(eventId);
try {
await onUnfollow?.(eventId);
} finally {
setUnfollowingId(null);
}
};
if (events.length === 0) { if (events.length === 0) {
return ( return (
<Box <Box
@@ -127,9 +155,9 @@ const EventsTabContent = ({ events, onEventClick, onAddEvent }) => {
> >
<VStack spacing={1.5} align="stretch"> <VStack spacing={1.5} align="stretch">
{events.map((event) => { {events.map((event) => {
const avgChg = event.related_avg_chg; const dailyChg = formatChange(event.daily_avg_chg);
const isUp = avgChg > 0; const weeklyChg = formatChange(event.weekly_chg);
const changeColor = isUp ? '#EF4444' : avgChg < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)'; const isUnfollowing = unfollowingId === event.id;
return ( return (
<Box <Box
@@ -141,27 +169,59 @@ const EventsTabContent = ({ events, onEventClick, onAddEvent }) => {
bg="rgba(37, 37, 64, 0.3)" bg="rgba(37, 37, 64, 0.3)"
_hover={{ bg: 'rgba(37, 37, 64, 0.6)' }} _hover={{ bg: 'rgba(37, 37, 64, 0.6)' }}
onClick={() => onEventClick?.(event)} onClick={() => onEventClick?.(event)}
role="group"
> >
<Text <Text
fontSize="xs" fontSize="xs"
fontWeight="medium" fontWeight="medium"
color="rgba(255, 255, 255, 0.9)" color="rgba(255, 255, 255, 0.9)"
noOfLines={2} noOfLines={2}
mb={1} mb={1.5}
lineHeight="1.4" lineHeight="1.4"
> >
{event.title} {event.title}
</Text> </Text>
<HStack justify="space-between" fontSize="10px"> {/* 底部:日均、周涨、取消 */}
<HStack spacing={1} color="rgba(255, 255, 255, 0.4)"> <HStack justify="space-between" fontSize="10px" flexWrap="wrap" gap={1}>
<Icon as={Users} boxSize={2.5} /> <HStack spacing={1.5}>
<Text>{event.follower_count || 0}</Text> {dailyChg && (
</HStack> <Badge
{avgChg !== undefined && avgChg !== null && ( bg="rgba(255, 255, 255, 0.08)"
<Text color={changeColor} fontWeight="medium"> color={dailyChg.color}
{isUp ? '+' : ''}{Number(avgChg).toFixed(2)}% fontSize="9px"
</Text> fontWeight="medium"
px={1.5}
py={0.5}
borderRadius="sm"
>
日均 {dailyChg.text}
</Badge>
)} )}
{weeklyChg && (
<Badge
bg="rgba(255, 255, 255, 0.08)"
color={weeklyChg.color}
fontSize="9px"
fontWeight="medium"
px={1.5}
py={0.5}
borderRadius="sm"
>
周涨 {weeklyChg.text}
</Badge>
)}
</HStack>
<Text
color="rgba(255, 255, 255, 0.4)"
fontSize="9px"
cursor="pointer"
opacity={0}
_groupHover={{ opacity: 1 }}
_hover={{ color: '#EF4444' }}
onClick={(e) => handleUnfollow(e, event.id)}
>
{isUnfollowing ? '取消中...' : '取消'}
</Text>
</HStack> </HStack>
</Box> </Box>
); );

View File

@@ -1,14 +1,27 @@
// 关注股票面板 - 紧凑版 // 关注股票面板 - 紧凑版
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, IconButton, Tooltip } from '@chakra-ui/react';
import { BarChart2, Plus } from 'lucide-react'; import { BarChart2, Plus, X } from 'lucide-react';
const WatchlistPanel = ({ const WatchlistPanel = ({
watchlist = [], watchlist = [],
realtimeQuotes = {}, realtimeQuotes = {},
onStockClick, onStockClick,
onAddStock, onAddStock,
onUnwatch,
}) => { }) => {
const [removingCode, setRemovingCode] = useState(null);
const handleUnwatch = async (e, stockCode) => {
e.stopPropagation();
if (removingCode) return;
setRemovingCode(stockCode);
try {
await onUnwatch?.(stockCode);
} finally {
setRemovingCode(null);
}
};
return ( return (
<Box> <Box>
{/* 标题 */} {/* 标题 */}
@@ -68,6 +81,8 @@ const WatchlistPanel = ({
const isUp = changePercent > 0; const isUp = changePercent > 0;
const changeColor = isUp ? '#EF4444' : changePercent < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)'; const changeColor = isUp ? '#EF4444' : changePercent < 0 ? '#22C55E' : 'rgba(255, 255, 255, 0.6)';
const isRemoving = removingCode === stock.stock_code;
return ( return (
<HStack <HStack
key={stock.stock_code} key={stock.stock_code}
@@ -78,6 +93,7 @@ const WatchlistPanel = ({
borderRadius="md" borderRadius="md"
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
onClick={() => onStockClick?.(stock)} onClick={() => onStockClick?.(stock)}
role="group"
> >
<VStack align="start" spacing={0} flex={1} minW={0}> <VStack align="start" spacing={0} flex={1} minW={0}>
<Text <Text
@@ -92,6 +108,7 @@ const WatchlistPanel = ({
{stock.stock_code} {stock.stock_code}
</Text> </Text>
</VStack> </VStack>
<HStack spacing={1}>
<VStack align="end" spacing={0}> <VStack align="end" spacing={0}>
<Text fontSize="xs" fontWeight="bold" color={changeColor}> <Text fontSize="xs" fontWeight="bold" color={changeColor}>
{quote?.current_price?.toFixed(2) || stock.current_price || '--'} {quote?.current_price?.toFixed(2) || stock.current_price || '--'}
@@ -102,6 +119,21 @@ const WatchlistPanel = ({
: '--'} : '--'}
</Text> </Text>
</VStack> </VStack>
<Tooltip label="取消关注" placement="top" hasArrow>
<IconButton
icon={<Icon as={X} boxSize={3} />}
size="xs"
variant="ghost"
color="rgba(255, 255, 255, 0.3)"
opacity={0}
_groupHover={{ opacity: 1 }}
_hover={{ color: '#EF4444', bg: 'rgba(239, 68, 68, 0.1)' }}
isLoading={isRemoving}
onClick={(e) => handleUnwatch(e, stock.stock_code)}
aria-label="取消关注"
/>
</Tooltip>
</HStack>
</HStack> </HStack>
); );
})} })}

View File

@@ -14,6 +14,8 @@ const WatchSidebar = ({
onCommentClick, onCommentClick,
onAddStock, onAddStock,
onAddEvent, onAddEvent,
onUnwatch,
onUnfollow,
}) => { }) => {
return ( return (
<VStack spacing={4} align="stretch"> <VStack spacing={4} align="stretch">
@@ -29,6 +31,7 @@ const WatchSidebar = ({
realtimeQuotes={realtimeQuotes} realtimeQuotes={realtimeQuotes}
onStockClick={onStockClick} onStockClick={onStockClick}
onAddStock={onAddStock} onAddStock={onAddStock}
onUnwatch={onUnwatch}
/> />
</GlassCard> </GlassCard>
@@ -45,6 +48,7 @@ const WatchSidebar = ({
onEventClick={onEventClick} onEventClick={onEventClick}
onCommentClick={onCommentClick} onCommentClick={onCommentClick}
onAddEvent={onAddEvent} onAddEvent={onAddEvent}
onUnfollow={onUnfollow}
/> />
</GlassCard> </GlassCard>
</VStack> </VStack>