feat(WatchSidebar): 面板 UI 优化,添加日均周涨展示
- WatchlistPanel: 添加 hideTitle 支持,新增日均/周涨 Badge 展示 - FollowingEventsPanel: 添加 hideTitle 支持,兼容 related_avg_chg 字段 - FollowingEventsMenu: 使用 FavoriteButton 替代文字按钮 - 统一卡片样式,与关注事件面板保持一致 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// src/components/Navbars/components/FeatureMenus/FollowingEventsMenu.js
|
// src/components/Navbars/components/FeatureMenus/FollowingEventsMenu.js
|
||||||
// 关注事件下拉菜单组件
|
// 关注事件下拉菜单组件
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
@@ -22,6 +22,7 @@ import { FiCalendar } from 'react-icons/fi';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useFollowingEvents } from '../../../../hooks/useFollowingEvents';
|
import { useFollowingEvents } from '../../../../hooks/useFollowingEvents';
|
||||||
import { getEventDetailUrl } from '@/utils/idEncoder';
|
import { getEventDetailUrl } from '@/utils/idEncoder';
|
||||||
|
import FavoriteButton from '@/components/FavoriteButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关注事件下拉菜单组件
|
* 关注事件下拉菜单组件
|
||||||
@@ -30,6 +31,7 @@ import { getEventDetailUrl } from '@/utils/idEncoder';
|
|||||||
*/
|
*/
|
||||||
const FollowingEventsMenu = memo(() => {
|
const FollowingEventsMenu = memo(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [unfollowingId, setUnfollowingId] = useState(null);
|
||||||
const {
|
const {
|
||||||
followingEvents,
|
followingEvents,
|
||||||
eventsLoading,
|
eventsLoading,
|
||||||
@@ -40,6 +42,17 @@ const FollowingEventsMenu = memo(() => {
|
|||||||
handleUnfollowEvent
|
handleUnfollowEvent
|
||||||
} = useFollowingEvents();
|
} = useFollowingEvents();
|
||||||
|
|
||||||
|
// 处理取消关注(带 loading 状态)
|
||||||
|
const handleUnfollow = async (eventId) => {
|
||||||
|
if (unfollowingId) return;
|
||||||
|
setUnfollowingId(eventId);
|
||||||
|
try {
|
||||||
|
await handleUnfollowEvent(eventId);
|
||||||
|
} finally {
|
||||||
|
setUnfollowingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const titleColor = useColorModeValue('gray.600', 'gray.300');
|
const titleColor = useColorModeValue('gray.600', 'gray.300');
|
||||||
const loadingTextColor = useColorModeValue('gray.500', 'gray.300');
|
const loadingTextColor = useColorModeValue('gray.500', 'gray.300');
|
||||||
const emptyTextColor = useColorModeValue('gray.500', 'gray.300');
|
const emptyTextColor = useColorModeValue('gray.500', 'gray.300');
|
||||||
@@ -108,27 +121,6 @@ const FollowingEventsMenu = memo(() => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
<HStack flexShrink={0} spacing={1}>
|
<HStack flexShrink={0} spacing={1}>
|
||||||
{/* 热度 */}
|
|
||||||
{typeof ev.hot_score === 'number' && (
|
|
||||||
<Badge
|
|
||||||
colorScheme={
|
|
||||||
ev.hot_score >= 80 ? 'red' :
|
|
||||||
(ev.hot_score >= 60 ? 'orange' : 'gray')
|
|
||||||
}
|
|
||||||
fontSize="xs"
|
|
||||||
>
|
|
||||||
🔥 {ev.hot_score}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{/* 关注数 */}
|
|
||||||
{typeof ev.follower_count === 'number' && ev.follower_count > 0 && (
|
|
||||||
<Badge
|
|
||||||
colorScheme="purple"
|
|
||||||
fontSize="xs"
|
|
||||||
>
|
|
||||||
👥 {ev.follower_count}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{/* 日均涨跌幅 */}
|
{/* 日均涨跌幅 */}
|
||||||
{typeof ev.related_avg_chg === 'number' && (
|
{typeof ev.related_avg_chg === 'number' && (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -155,23 +147,21 @@ const FollowingEventsMenu = memo(() => {
|
|||||||
{ev.related_week_chg.toFixed(2)}%
|
{ev.related_week_chg.toFixed(2)}%
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{/* 取消关注按钮 */}
|
{/* 取消关注按钮 - 使用 FavoriteButton */}
|
||||||
<Box
|
<Box
|
||||||
as="span"
|
|
||||||
fontSize="xs"
|
|
||||||
color="red.500"
|
|
||||||
cursor="pointer"
|
|
||||||
px={2}
|
|
||||||
py={1}
|
|
||||||
borderRadius="md"
|
|
||||||
_hover={{ bg: 'red.50' }}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleUnfollowEvent(ev.id);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
取消
|
<FavoriteButton
|
||||||
|
isFavorite={true}
|
||||||
|
isLoading={unfollowingId === ev.id}
|
||||||
|
onClick={() => handleUnfollow(ev.id)}
|
||||||
|
size="sm"
|
||||||
|
colorScheme="gold"
|
||||||
|
showTooltip={true}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
|
|||||||
import { Box, Text, VStack, HStack, Icon, Button, ButtonGroup, Badge } 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';
|
||||||
|
import FavoriteButton from '@/components/FavoriteButton';
|
||||||
|
|
||||||
const TAB_EVENTS = 'events';
|
const TAB_EVENTS = 'events';
|
||||||
const TAB_COMMENTS = 'comments';
|
const TAB_COMMENTS = 'comments';
|
||||||
@@ -14,12 +15,14 @@ const FollowingEventsPanel = ({
|
|||||||
onCommentClick,
|
onCommentClick,
|
||||||
onAddEvent,
|
onAddEvent,
|
||||||
onUnfollow,
|
onUnfollow,
|
||||||
|
hideTitle = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState(TAB_EVENTS);
|
const [activeTab, setActiveTab] = useState(TAB_EVENTS);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 标题栏 + Tab 切换 */}
|
{/* 标题栏 - 可隐藏 */}
|
||||||
|
{!hideTitle && (
|
||||||
<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)" />
|
||||||
@@ -36,6 +39,7 @@ const FollowingEventsPanel = ({
|
|||||||
onClick={onAddEvent}
|
onClick={onAddEvent}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tab 切换按钮 */}
|
{/* Tab 切换按钮 */}
|
||||||
<ButtonGroup size="xs" isAttached variant="outline" mb={2} w="100%">
|
<ButtonGroup size="xs" isAttached variant="outline" mb={2} w="100%">
|
||||||
@@ -110,8 +114,7 @@ const formatChange = (value) => {
|
|||||||
const EventsTabContent = ({ events, onEventClick, onAddEvent, onUnfollow }) => {
|
const EventsTabContent = ({ events, onEventClick, onAddEvent, onUnfollow }) => {
|
||||||
const [unfollowingId, setUnfollowingId] = useState(null);
|
const [unfollowingId, setUnfollowingId] = useState(null);
|
||||||
|
|
||||||
const handleUnfollow = async (e, eventId) => {
|
const handleUnfollow = async (eventId) => {
|
||||||
e.stopPropagation();
|
|
||||||
if (unfollowingId) return;
|
if (unfollowingId) return;
|
||||||
setUnfollowingId(eventId);
|
setUnfollowingId(eventId);
|
||||||
try {
|
try {
|
||||||
@@ -155,8 +158,9 @@ const EventsTabContent = ({ events, onEventClick, onAddEvent, onUnfollow }) => {
|
|||||||
>
|
>
|
||||||
<VStack spacing={1.5} align="stretch">
|
<VStack spacing={1.5} align="stretch">
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
const dailyChg = formatChange(event.daily_avg_chg);
|
// 兼容两种字段名:daily_avg_chg / related_avg_chg, weekly_chg / related_week_chg
|
||||||
const weeklyChg = formatChange(event.weekly_chg);
|
const dailyChg = formatChange(event.daily_avg_chg ?? event.related_avg_chg);
|
||||||
|
const weeklyChg = formatChange(event.weekly_chg ?? event.related_week_chg);
|
||||||
const isUnfollowing = unfollowingId === event.id;
|
const isUnfollowing = unfollowingId === event.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -181,7 +185,7 @@ const EventsTabContent = ({ events, onEventClick, onAddEvent, onUnfollow }) => {
|
|||||||
>
|
>
|
||||||
{event.title}
|
{event.title}
|
||||||
</Text>
|
</Text>
|
||||||
{/* 底部:日均、周涨、取消 */}
|
{/* 底部:日均、周涨、取消关注按钮 */}
|
||||||
<HStack justify="space-between" fontSize="10px" flexWrap="wrap" gap={1}>
|
<HStack justify="space-between" fontSize="10px" flexWrap="wrap" gap={1}>
|
||||||
<HStack spacing={1.5}>
|
<HStack spacing={1.5}>
|
||||||
{dailyChg && (
|
{dailyChg && (
|
||||||
@@ -211,17 +215,16 @@ const EventsTabContent = ({ events, onEventClick, onAddEvent, onUnfollow }) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text
|
<Box onClick={(e) => e.stopPropagation()}>
|
||||||
color="rgba(255, 255, 255, 0.4)"
|
<FavoriteButton
|
||||||
fontSize="9px"
|
isFavorite={true}
|
||||||
cursor="pointer"
|
isLoading={isUnfollowing}
|
||||||
opacity={0}
|
onClick={() => handleUnfollow(event.id)}
|
||||||
_groupHover={{ opacity: 1 }}
|
size="sm"
|
||||||
_hover={{ color: '#EF4444' }}
|
colorScheme="gold"
|
||||||
onClick={(e) => handleUnfollow(e, event.id)}
|
showTooltip={true}
|
||||||
>
|
/>
|
||||||
{isUnfollowing ? '取消中...' : '取消'}
|
</Box>
|
||||||
</Text>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
// 关注股票面板 - 紧凑版
|
// 关注股票面板 - 紧凑版
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Box, Text, VStack, HStack, Icon, IconButton, Tooltip } from '@chakra-ui/react';
|
import { Box, Text, VStack, HStack, Icon, Badge } from '@chakra-ui/react';
|
||||||
import { BarChart2, Plus, X } from 'lucide-react';
|
import { BarChart2, Plus } from 'lucide-react';
|
||||||
|
import FavoriteButton from '@/components/FavoriteButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化涨跌幅
|
||||||
|
*/
|
||||||
|
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)',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const WatchlistPanel = ({
|
const WatchlistPanel = ({
|
||||||
watchlist = [],
|
watchlist = [],
|
||||||
@@ -9,11 +23,11 @@ const WatchlistPanel = ({
|
|||||||
onStockClick,
|
onStockClick,
|
||||||
onAddStock,
|
onAddStock,
|
||||||
onUnwatch,
|
onUnwatch,
|
||||||
|
hideTitle = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [removingCode, setRemovingCode] = useState(null);
|
const [removingCode, setRemovingCode] = useState(null);
|
||||||
|
|
||||||
const handleUnwatch = async (e, stockCode) => {
|
const handleUnwatch = async (stockCode) => {
|
||||||
e.stopPropagation();
|
|
||||||
if (removingCode) return;
|
if (removingCode) return;
|
||||||
setRemovingCode(stockCode);
|
setRemovingCode(stockCode);
|
||||||
try {
|
try {
|
||||||
@@ -24,7 +38,8 @@ const WatchlistPanel = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 标题 */}
|
{/* 标题 - 可隐藏 */}
|
||||||
|
{!hideTitle && (
|
||||||
<HStack justify="space-between" mb={2}>
|
<HStack justify="space-between" mb={2}>
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
<Icon as={BarChart2} boxSize={3.5} color="rgba(59, 130, 246, 0.9)" />
|
<Icon as={BarChart2} boxSize={3.5} color="rgba(59, 130, 246, 0.9)" />
|
||||||
@@ -44,6 +59,7 @@ const WatchlistPanel = ({
|
|||||||
onClick={onAddStock}
|
onClick={onAddStock}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 股票列表 - 固定高度可滚动 */}
|
{/* 股票列表 - 固定高度可滚动 */}
|
||||||
{watchlist.length === 0 ? (
|
{watchlist.length === 0 ? (
|
||||||
@@ -74,7 +90,7 @@ const WatchlistPanel = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VStack spacing={1} align="stretch">
|
<VStack spacing={1.5} align="stretch">
|
||||||
{watchlist.map((stock) => {
|
{watchlist.map((stock) => {
|
||||||
const quote = realtimeQuotes[stock.stock_code];
|
const quote = realtimeQuotes[stock.stock_code];
|
||||||
const changePercent = quote?.change_percent ?? stock.change_percent;
|
const changePercent = quote?.change_percent ?? stock.change_percent;
|
||||||
@@ -83,18 +99,24 @@ const WatchlistPanel = ({
|
|||||||
|
|
||||||
const isRemoving = removingCode === stock.stock_code;
|
const isRemoving = removingCode === stock.stock_code;
|
||||||
|
|
||||||
|
// 日均涨跌幅和周涨跌幅
|
||||||
|
const dailyChg = formatChange(quote?.daily_avg_chg ?? stock.daily_avg_chg);
|
||||||
|
const weeklyChg = formatChange(quote?.weekly_chg ?? stock.weekly_chg);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<Box
|
||||||
key={stock.stock_code}
|
key={stock.stock_code}
|
||||||
py={1.5}
|
py={2}
|
||||||
px={2}
|
px={2}
|
||||||
justify="space-between"
|
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
bg="rgba(37, 37, 64, 0.3)"
|
||||||
|
_hover={{ bg: 'rgba(37, 37, 64, 0.6)' }}
|
||||||
onClick={() => onStockClick?.(stock)}
|
onClick={() => onStockClick?.(stock)}
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
|
{/* 第一行:股票名称 + 价格/涨跌幅 + 取消关注按钮 */}
|
||||||
|
<HStack justify="space-between" mb={1.5}>
|
||||||
<VStack align="start" spacing={0} flex={1} minW={0}>
|
<VStack align="start" spacing={0} flex={1} minW={0}>
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
@@ -119,22 +141,50 @@ const WatchlistPanel = ({
|
|||||||
: '--'}
|
: '--'}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<Tooltip label="取消关注" placement="top" hasArrow>
|
<Box onClick={(e) => e.stopPropagation()}>
|
||||||
<IconButton
|
<FavoriteButton
|
||||||
icon={<Icon as={X} boxSize={3} />}
|
isFavorite={true}
|
||||||
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}
|
isLoading={isRemoving}
|
||||||
onClick={(e) => handleUnwatch(e, stock.stock_code)}
|
onClick={() => handleUnwatch(stock.stock_code)}
|
||||||
aria-label="取消关注"
|
size="sm"
|
||||||
|
colorScheme="gold"
|
||||||
|
showTooltip={true}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Box>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{/* 第二行:日均、周涨 Badge */}
|
||||||
|
{(dailyChg || weeklyChg) && (
|
||||||
|
<HStack spacing={1.5} fontSize="10px">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
Reference in New Issue
Block a user