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:
@@ -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 && (
|
||||||
|
<Badge
|
||||||
|
bg="rgba(255, 255, 255, 0.08)"
|
||||||
|
color={dailyChg.color}
|
||||||
|
fontSize="9px"
|
||||||
|
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>
|
</HStack>
|
||||||
{avgChg !== undefined && avgChg !== null && (
|
<Text
|
||||||
<Text color={changeColor} fontWeight="medium">
|
color="rgba(255, 255, 255, 0.4)"
|
||||||
{isUp ? '+' : ''}{Number(avgChg).toFixed(2)}%
|
fontSize="9px"
|
||||||
</Text>
|
cursor="pointer"
|
||||||
)}
|
opacity={0}
|
||||||
|
_groupHover={{ opacity: 1 }}
|
||||||
|
_hover={{ color: '#EF4444' }}
|
||||||
|
onClick={(e) => handleUnfollow(e, event.id)}
|
||||||
|
>
|
||||||
|
{isUnfollowing ? '取消中...' : '取消'}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,16 +108,32 @@ const WatchlistPanel = ({
|
|||||||
{stock.stock_code}
|
{stock.stock_code}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack align="end" spacing={0}>
|
<HStack spacing={1}>
|
||||||
<Text fontSize="xs" fontWeight="bold" color={changeColor}>
|
<VStack align="end" spacing={0}>
|
||||||
{quote?.current_price?.toFixed(2) || stock.current_price || '--'}
|
<Text fontSize="xs" fontWeight="bold" color={changeColor}>
|
||||||
</Text>
|
{quote?.current_price?.toFixed(2) || stock.current_price || '--'}
|
||||||
<Text fontSize="10px" color={changeColor}>
|
</Text>
|
||||||
{changePercent !== undefined && changePercent !== null
|
<Text fontSize="10px" color={changeColor}>
|
||||||
? `${isUp ? '+' : ''}${Number(changePercent).toFixed(2)}%`
|
{changePercent !== undefined && changePercent !== null
|
||||||
: '--'}
|
? `${isUp ? '+' : ''}${Number(changePercent).toFixed(2)}%`
|
||||||
</Text>
|
: '--'}
|
||||||
</VStack>
|
</Text>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user