feat: 添加导航徽章

This commit is contained in:
zdl
2025-10-20 13:28:37 +08:00
parent 923611f3a8
commit 44f9fea624
10 changed files with 2677 additions and 1113 deletions

View File

@@ -59,8 +59,7 @@ import {
FiAlertCircle,
} from 'react-icons/fi';
import MyFutureEvents from './components/MyFutureEvents';
import InvestmentCalendarChakra from './components/InvestmentCalendarChakra';
import InvestmentPlansAndReviews from './components/InvestmentPlansAndReviews';
import InvestmentPlanningCenter from './components/InvestmentPlanningCenter';
export default function CenterDashboard() {
const { user } = useAuth();
@@ -81,26 +80,21 @@ export default function CenterDashboard() {
const [realtimeQuotes, setRealtimeQuotes] = useState({});
const [followingEvents, setFollowingEvents] = useState([]);
const [eventComments, setEventComments] = useState([]);
const [subscriptionInfo, setSubscriptionInfo] = useState({ type: 'free', status: 'active', days_left: 999, is_active: true });
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [quotesLoading, setQuotesLoading] = useState(false);
const loadData = useCallback(async () => {
try {
setRefreshing(true);
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
const ts = Date.now();
const [w, e, c, s] = await Promise.all([
const [w, e, c] = await Promise.all([
fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/account/events/following?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/account/events/comments?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/subscription/current?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
]);
const jw = await w.json();
const je = await e.json();
const jc = await c.json();
const js = await s.json();
if (jw.success) {
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
// 加载实时行情
@@ -110,18 +104,15 @@ export default function CenterDashboard() {
}
if (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
if (js.success) setSubscriptionInfo(js.data);
} catch (err) {
// ❌ 移除 toast仅 console 输出
logger.error('Center', 'loadData', err, {
userId: user?.id,
timestamp: new Date().toISOString()
});
} finally {
setLoading(false);
setRefreshing(false);
}
}, [user]); // ✅ 移除 toast 依赖
}, [user]);
// 加载实时行情
const loadRealtimeQuotes = useCallback(async () => {
@@ -235,96 +226,11 @@ export default function CenterDashboard() {
return (
<Box bg={sectionBg} minH="100vh">
<Box px={{ base: 4, md: 8 }} py={6} maxW="1400px" mx="auto">
{/* 头部 */}
<Flex justify="space-between" align="center" mb={8}>
<VStack align="start" spacing={1}>
<Heading size="lg" color={textColor}>
个人中心
</Heading>
<Text color={secondaryText} fontSize="sm">
管理您的自选股事件关注和互动记录
</Text>
</VStack>
<Button
leftIcon={<FiRefreshCw />}
onClick={loadData}
isLoading={refreshing}
loadingText="刷新中"
variant="solid"
colorScheme="blue"
size="sm"
>
刷新数据
</Button>
</Flex>
{/* 统计卡片 */}
<SimpleGrid columns={{ base: 1, sm: 2, md: 4 }} spacing={4} mb={8}>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>自选股票</StatLabel>
<StatNumber fontSize="2xl">{watchlist.length}</StatNumber>
<StatHelpText>
<Icon as={FiTrendingUp} color="green.500" mr={1} />
关注市场动态
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>关注事件</StatLabel>
<StatNumber fontSize="2xl">{followingEvents.length}</StatNumber>
<StatHelpText>
<Icon as={FiActivity} color="blue.500" mr={1} />
追踪热点事件
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>我的评论</StatLabel>
<StatNumber fontSize="2xl">{eventComments.length}</StatNumber>
<StatHelpText>
<Icon as={FiMessageSquare} color="purple.500" mr={1} />
参与讨论
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm" cursor="pointer" onClick={() => navigate('/home/pages/account/subscription')} _hover={{ transform: 'translateY(-2px)', shadow: 'lg' }} transition="all 0.2s">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>订阅状态</StatLabel>
<StatNumber fontSize="xl" color={subscriptionInfo.type === 'free' ? 'gray.500' : subscriptionInfo.type === 'pro' ? 'blue.500' : 'purple.500'}>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
</StatNumber>
<StatHelpText>
<Icon as={FiStar} color={subscriptionInfo.type === 'free' ? 'gray.400' : 'orange.400'} mr={1} />
{subscriptionInfo.type === 'free' ? '点击升级' : `剩余${subscriptionInfo.days_left}`}
</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
{/* 投资日历 */}
<Box mb={8}>
<InvestmentCalendarChakra />
</Box>
{/* 主要内容区域 */}
<Grid templateColumns={{ base: '1fr', lg: '1fr 2fr' }} gap={6}>
{/* 左:自选股 */}
<Grid templateColumns={{ base: '1fr', md: '1fr 1fr', lg: 'repeat(3, 1fr)' }} gap={6} mb={8}>
{/* 左:自选股 */}
<VStack spacing={6} align="stretch">
<Card bg={cardBg} shadow="md">
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
@@ -335,26 +241,16 @@ export default function CenterDashboard() {
</Badge>
{quotesLoading && <Spinner size="sm" color="blue.500" />}
</HStack>
<HStack>
<IconButton
icon={<FiRefreshCw />}
variant="ghost"
size="sm"
onClick={loadRealtimeQuotes}
isLoading={quotesLoading}
aria-label="刷新行情"
/>
<IconButton
icon={<FiPlus />}
variant="ghost"
size="sm"
onClick={() => navigate('/stock-analysis/overview')}
aria-label="添加自选股"
/>
</HStack>
<IconButton
icon={<FiPlus />}
variant="ghost"
size="sm"
onClick={() => navigate('/stock-analysis/overview')}
aria-label="添加自选股"
/>
</Flex>
</CardHeader>
<CardBody pt={0}>
<CardBody pt={0} flex="1" overflowY="auto">
{watchlist.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
@@ -440,86 +336,12 @@ export default function CenterDashboard() {
)}
</CardBody>
</Card>
{/* 订阅管理 */}
<Card bg={cardBg} shadow="md">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
<Icon as={FiStar} color={subscriptionInfo.type === 'free' ? 'gray.500' : subscriptionInfo.type === 'pro' ? 'blue.500' : 'purple.500'} boxSize={5} />
<Heading size="md">我的订阅</Heading>
<Badge
colorScheme={subscriptionInfo.type === 'free' ? 'gray' : subscriptionInfo.type === 'pro' ? 'blue' : 'purple'}
variant="subtle"
>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
</Badge>
</HStack>
<Button
size="sm"
variant="ghost"
colorScheme={subscriptionInfo.type === 'free' ? 'blue' : 'purple'}
onClick={() => navigate('/home/pages/account/subscription')}
>
{subscriptionInfo.type === 'free' ? '升级' : '管理'}
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
<VStack align="stretch" spacing={4}>
<Box p={4} borderRadius="md" bg={subscriptionInfo.type === 'free' ? 'gray.50' : subscriptionInfo.type === 'pro' ? 'blue.50' : 'purple.50'} border="1px" borderColor={subscriptionInfo.type === 'free' ? 'gray.200' : subscriptionInfo.type === 'pro' ? 'blue.200' : 'purple.200'}>
<HStack justify="space-between">
<VStack align="start" spacing={1}>
<Text fontSize="sm" fontWeight="medium" color={textColor}>
当前套餐
</Text>
<Text fontSize="lg" fontWeight="bold" color={subscriptionInfo.type === 'free' ? 'gray.600' : subscriptionInfo.type === 'pro' ? 'blue.600' : 'purple.600'}>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版本' : 'Max版本'}
</Text>
</VStack>
<VStack align="end" spacing={1}>
<Text fontSize="sm" color={secondaryText}>
{subscriptionInfo.type === 'free' ? '永久免费' : subscriptionInfo.is_active ? '已激活' : '已过期'}
</Text>
{subscriptionInfo.type !== 'free' && (
<Text fontSize="xs" color={subscriptionInfo.days_left > 7 ? 'green.500' : 'orange.500'}>
剩余 {subscriptionInfo.days_left}
</Text>
)}
</VStack>
</HStack>
</Box>
{subscriptionInfo.type === 'free' ? (
<VStack spacing={2}>
<Text fontSize="sm" color={secondaryText} textAlign="center">
升级到Pro或Max版本解锁更多功能
</Text>
<HStack spacing={2}>
<Button size="xs" colorScheme="blue" variant="outline" onClick={() => navigate('/home/pages/account/subscription')}>
Pro ¥0.01/
</Button>
<Button size="xs" colorScheme="purple" variant="outline" onClick={() => navigate('/home/pages/account/subscription')}>
Max ¥0.1/
</Button>
</HStack>
</VStack>
) : (
<Box textAlign="center">
<Text fontSize="sm" color={subscriptionInfo.is_active ? 'green.600' : 'orange.600'}>
{subscriptionInfo.is_active ? '订阅服务正常' : '订阅已过期,请续费'}
</Text>
</Box>
)}
</VStack>
</CardBody>
</Card>
</VStack>
{/* 右侧:事件相关 */}
{/* 中列:关注事件 */}
<VStack spacing={6} align="stretch">
{/* 关注事件 */}
<Card bg={cardBg} shadow="md">
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
@@ -538,7 +360,7 @@ export default function CenterDashboard() {
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
<CardBody pt={0} flex="1" overflowY="auto">
{followingEvents.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
@@ -651,10 +473,12 @@ export default function CenterDashboard() {
</CardBody>
</Card>
{/* 移除“未来事件”板块,根据需求不再展示 */}
</VStack>
{/* 右列:我的评论 */}
<VStack spacing={6} align="stretch">
{/* 我的评论 */}
<Card bg={cardBg} shadow="md">
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
@@ -666,7 +490,7 @@ export default function CenterDashboard() {
</HStack>
</Flex>
</CardHeader>
<CardBody pt={0}>
<CardBody pt={0} flex="1" overflowY="auto">
{eventComments.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
@@ -723,9 +547,9 @@ export default function CenterDashboard() {
</VStack>
</Grid>
{/* 我的复盘和计划 */}
<Box mt={8}>
<InvestmentPlansAndReviews />
{/* 投资规划中心(整合了日历、计划、复盘) */}
<Box>
<InvestmentPlanningCenter />
</Box>
</Box>
</Box>