diff --git a/src/components/GlassCard/index.js b/src/components/GlassCard/index.js new file mode 100644 index 00000000..c5385bda --- /dev/null +++ b/src/components/GlassCard/index.js @@ -0,0 +1,179 @@ +/** + * GlassCard - 通用毛玻璃卡片组件 + * + * 复用自 Company 页面的 Glassmorphism 风格 + * 可在全局使用 + */ + +import React, { memo, forwardRef } from 'react'; +import { Box } from '@chakra-ui/react'; + +// 主题配置 +const GLASS_THEME = { + colors: { + gold: { + 400: '#D4AF37', + 500: '#B8960C', + }, + bg: { + deep: '#0A0A14', + primary: '#0F0F1A', + elevated: '#1A1A2E', + surface: '#252540', + }, + line: { + subtle: 'rgba(212, 175, 55, 0.1)', + default: 'rgba(212, 175, 55, 0.2)', + emphasis: 'rgba(212, 175, 55, 0.4)', + }, + }, + blur: { + sm: 'blur(8px)', + md: 'blur(16px)', + lg: 'blur(24px)', + }, + glow: { + sm: '0 0 8px rgba(212, 175, 55, 0.3)', + md: '0 0 16px rgba(212, 175, 55, 0.4)', + }, +}; + +// 变体样式 +const VARIANTS = { + default: { + bg: `linear-gradient(135deg, ${GLASS_THEME.colors.bg.elevated} 0%, ${GLASS_THEME.colors.bg.primary} 100%)`, + border: `1px solid ${GLASS_THEME.colors.line.default}`, + backdropFilter: GLASS_THEME.blur.md, + }, + elevated: { + bg: `linear-gradient(145deg, ${GLASS_THEME.colors.bg.surface} 0%, ${GLASS_THEME.colors.bg.elevated} 100%)`, + border: `1px solid ${GLASS_THEME.colors.line.emphasis}`, + backdropFilter: GLASS_THEME.blur.lg, + }, + subtle: { + bg: 'rgba(212, 175, 55, 0.05)', + border: `1px solid ${GLASS_THEME.colors.line.subtle}`, + backdropFilter: GLASS_THEME.blur.sm, + }, + transparent: { + bg: 'rgba(15, 15, 26, 0.8)', + border: `1px solid ${GLASS_THEME.colors.line.default}`, + backdropFilter: GLASS_THEME.blur.lg, + }, +}; + +const ROUNDED_MAP = { + sm: '8px', + md: '12px', + lg: '16px', + xl: '20px', + '2xl': '24px', +}; + +const PADDING_MAP = { + none: 0, + sm: 3, + md: 4, + lg: 6, +}; + +// 角落装饰 +const CornerDecor = memo(({ position }) => { + const baseStyle = { + position: 'absolute', + width: '12px', + height: '12px', + borderColor: GLASS_THEME.colors.gold[400], + borderStyle: 'solid', + borderWidth: 0, + opacity: 0.6, + }; + + const positions = { + tl: { top: '8px', left: '8px', borderTopWidth: '2px', borderLeftWidth: '2px' }, + tr: { top: '8px', right: '8px', borderTopWidth: '2px', borderRightWidth: '2px' }, + bl: { bottom: '8px', left: '8px', borderBottomWidth: '2px', borderLeftWidth: '2px' }, + br: { bottom: '8px', right: '8px', borderBottomWidth: '2px', borderRightWidth: '2px' }, + }; + + return ; +}); + +CornerDecor.displayName = 'CornerDecor'; + +/** + * GlassCard 组件 + * + * @param {string} variant - 变体: 'default' | 'elevated' | 'subtle' | 'transparent' + * @param {boolean} hoverable - 是否启用悬停效果 + * @param {boolean} glowing - 是否启用发光效果 + * @param {boolean} cornerDecor - 是否显示角落装饰 + * @param {string} rounded - 圆角: 'sm' | 'md' | 'lg' | 'xl' | '2xl' + * @param {string} padding - 内边距: 'none' | 'sm' | 'md' | 'lg' + */ +const GlassCard = forwardRef( + ( + { + children, + variant = 'default', + hoverable = true, + glowing = false, + cornerDecor = false, + rounded = 'lg', + padding = 'md', + ...props + }, + ref + ) => { + const variantStyle = VARIANTS[variant] || VARIANTS.default; + + return ( + + {/* 角落装饰 */} + {cornerDecor && ( + <> + + + + + + )} + + {/* 内容 */} + + {children} + + + ); + } +); + +GlassCard.displayName = 'GlassCard'; + +export default memo(GlassCard); +export { GLASS_THEME }; diff --git a/src/routes/lazy-components.js b/src/routes/lazy-components.js index 37fed848..b383e89a 100644 --- a/src/routes/lazy-components.js +++ b/src/routes/lazy-components.js @@ -11,7 +11,7 @@ export const lazyComponents = { // Home 模块 // ⚡ 直接引用 HomePage,无需中间层(静态页面不需要骨架屏) HomePage: React.lazy(() => import('@views/Home/HomePage')), - CenterDashboard: React.lazy(() => import('@views/Dashboard/Center')), + CenterDashboard: React.lazy(() => import('@views/Center')), ProfilePage: React.lazy(() => import('@views/Profile/ProfilePage')), // 价值论坛 - 我的积分页面 ForumMyPoints: React.lazy(() => import('@views/Profile')), diff --git a/src/types/center.ts b/src/types/center.ts new file mode 100644 index 00000000..a0e377a4 --- /dev/null +++ b/src/types/center.ts @@ -0,0 +1,349 @@ +/** + * Center(个人中心)模块类型定义 + * + * 包含自选股、实时行情、关注事件等类型 + */ + +import type { NavigateFunction } from 'react-router-dom'; + +// ============================================================ +// Dashboard Events Hook 类型定义 +// ============================================================ + +/** + * useDashboardEvents Hook 配置选项 + */ +export interface DashboardEventsOptions { + /** 页面类型 */ + pageType?: 'center' | 'profile' | 'settings'; + /** 路由导航函数 */ + navigate?: NavigateFunction; +} + +/** + * useDashboardEvents Hook 返回值 + */ +export interface DashboardEventsResult { + /** 追踪功能卡片点击 */ + trackFunctionCardClicked: (cardName: string, cardData?: { count?: number }) => void; + /** 追踪自选股列表查看 */ + trackWatchlistViewed: (stockCount?: number, hasRealtime?: boolean) => void; + /** 追踪自选股点击 */ + trackWatchlistStockClicked: (stock: { code: string; name?: string }, position?: number) => void; + /** 追踪自选股添加 */ + trackWatchlistStockAdded: (stock: { code: string; name?: string }, source?: string) => void; + /** 追踪自选股移除 */ + trackWatchlistStockRemoved: (stock: { code: string; name?: string }) => void; + /** 追踪关注事件列表查看 */ + trackFollowingEventsViewed: (eventCount?: number) => void; + /** 追踪关注事件点击 */ + trackFollowingEventClicked: (event: { id: number; title?: string }, position?: number) => void; + /** 追踪评论列表查看 */ + trackCommentsViewed: (commentCount?: number) => void; + /** 追踪订阅信息查看 */ + trackSubscriptionViewed: (subscription?: { plan?: string; status?: string }) => void; + /** 追踪升级按钮点击 */ + trackUpgradePlanClicked: (currentPlan?: string, targetPlan?: string, source?: string) => void; + /** 追踪个人资料更新 */ + trackProfileUpdated: (updatedFields?: string[]) => void; + /** 追踪设置更改 */ + trackSettingChanged: (settingName: string, oldValue: unknown, newValue: unknown) => void; +} + +/** + * 自选股项目 + * 来自 /api/account/watchlist 接口 + */ +export interface WatchlistItem { + /** 股票代码(如 '600000.SH') */ + stock_code: string; + + /** 股票名称 */ + stock_name: string; + + /** 当前价格 */ + current_price?: number; + + /** 涨跌幅(百分比) */ + change_percent?: number; + + /** 添加时间 */ + created_at?: string; + + /** 备注 */ + note?: string; +} + +/** + * 实时行情数据 + * 来自 /api/account/watchlist/realtime 接口 + */ +export interface RealtimeQuote { + /** 股票代码 */ + stock_code: string; + + /** 当前价格 */ + current_price: number; + + /** 涨跌幅(百分比) */ + change_percent: number; + + /** 涨跌额 */ + change_amount?: number; + + /** 成交量 */ + volume?: number; + + /** 成交额 */ + amount?: number; + + /** 最高价 */ + high?: number; + + /** 最低价 */ + low?: number; + + /** 开盘价 */ + open?: number; + + /** 昨收价 */ + pre_close?: number; + + /** 更新时间戳 */ + timestamp?: number; +} + +/** + * 实时行情映射表 + * key 为股票代码,value 为行情数据 + */ +export type RealtimeQuotesMap = Record; + +/** + * 关注的事件 + * 来自 /api/account/events/following 接口 + */ +export interface FollowingEvent { + /** 事件 ID */ + id: number; + + /** 事件标题 */ + title: string; + + /** 关注人数 */ + follower_count?: number; + + /** 相关股票平均涨跌幅(百分比) */ + related_avg_chg?: number; + + /** 事件类型 */ + event_type?: string; + + /** 发生日期 */ + event_date?: string; + + /** 事件描述 */ + description?: string; + + /** 相关股票列表 */ + related_stocks?: Array<{ + code: string; + name: string; + change_percent?: number; + }>; + + /** 创建时间 */ + created_at?: string; +} + +/** + * 用户评论记录 + * 来自 /api/account/events/posts 接口 + */ +export interface EventComment { + /** 评论 ID */ + id: number; + + /** 评论内容 */ + content: string; + + /** 关联事件 ID */ + event_id: number; + + /** 关联事件标题 */ + event_title?: string; + + /** 点赞数 */ + like_count?: number; + + /** 回复数 */ + reply_count?: number; + + /** 创建时间 */ + created_at: string; + + /** 更新时间 */ + updated_at?: string; +} + +// ============================================================ +// 组件 Props 类型定义 +// ============================================================ + +/** + * WatchSidebar 组件 Props + */ +export interface WatchSidebarProps { + /** 自选股列表 */ + watchlist: WatchlistItem[]; + + /** 实时行情数据(按股票代码索引) */ + realtimeQuotes: RealtimeQuotesMap; + + /** 关注的事件列表 */ + followingEvents: FollowingEvent[]; + + /** 点击股票回调 */ + onStockClick?: (stock: WatchlistItem) => void; + + /** 点击事件回调 */ + onEventClick?: (event: FollowingEvent) => void; + + /** 添加股票回调 */ + onAddStock?: () => void; + + /** 添加事件回调 */ + onAddEvent?: () => void; +} + +/** + * WatchlistPanel 组件 Props + */ +export interface WatchlistPanelProps { + /** 自选股列表 */ + watchlist: WatchlistItem[]; + + /** 实时行情数据 */ + realtimeQuotes: RealtimeQuotesMap; + + /** 点击股票回调 */ + onStockClick?: (stock: WatchlistItem) => void; + + /** 添加股票回调 */ + onAddStock?: () => void; +} + +/** + * FollowingEventsPanel 组件 Props + */ +export interface FollowingEventsPanelProps { + /** 事件列表 */ + events: FollowingEvent[]; + + /** 点击事件回调 */ + onEventClick?: (event: FollowingEvent) => void; + + /** 添加事件回调 */ + onAddEvent?: () => void; +} + +// ============================================================ +// Hooks 返回值类型定义 +// ============================================================ + +/** + * useCenterColors Hook 返回值 + * 封装 Center 模块的所有颜色变量 + */ +export interface CenterColors { + /** 主要文本颜色 */ + textColor: string; + + /** 边框颜色 */ + borderColor: string; + + /** 背景颜色 */ + bgColor: string; + + /** 悬停背景色 */ + hoverBg: string; + + /** 次要文本颜色 */ + secondaryText: string; + + /** 卡片背景色 */ + cardBg: string; + + /** 区块背景色 */ + sectionBg: string; +} + +/** + * useCenterData Hook 返回值 + * 封装 Center 页面的数据加载逻辑 + */ +export interface UseCenterDataResult { + /** 自选股列表 */ + watchlist: WatchlistItem[]; + + /** 实时行情数据 */ + realtimeQuotes: RealtimeQuotesMap; + + /** 关注的事件列表 */ + followingEvents: FollowingEvent[]; + + /** 用户评论列表 */ + eventComments: EventComment[]; + + /** 加载状态 */ + loading: boolean; + + /** 行情加载状态 */ + quotesLoading: boolean; + + /** 刷新数据 */ + refresh: () => Promise; + + /** 刷新实时行情 */ + refreshQuotes: () => Promise; +} + +// ============================================================ +// API 响应类型定义 +// ============================================================ + +/** + * 自选股列表 API 响应 + */ +export interface WatchlistApiResponse { + success: boolean; + data: WatchlistItem[]; + message?: string; +} + +/** + * 实时行情 API 响应 + */ +export interface RealtimeQuotesApiResponse { + success: boolean; + data: RealtimeQuote[]; + message?: string; +} + +/** + * 关注事件 API 响应 + */ +export interface FollowingEventsApiResponse { + success: boolean; + data: FollowingEvent[]; + message?: string; +} + +/** + * 用户评论 API 响应 + */ +export interface EventCommentsApiResponse { + success: boolean; + data: EventComment[]; + message?: string; +} diff --git a/src/types/index.ts b/src/types/index.ts index 4c1540e9..0ea713f8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -63,3 +63,23 @@ export type { PlanFormData, PlanningContextValue, } from './investment'; + +// Center(个人中心)相关类型 +export type { + DashboardEventsOptions, + DashboardEventsResult, + WatchlistItem, + RealtimeQuote, + RealtimeQuotesMap, + FollowingEvent, + EventComment, + WatchSidebarProps, + WatchlistPanelProps, + FollowingEventsPanelProps, + CenterColors, + UseCenterDataResult, + WatchlistApiResponse, + RealtimeQuotesApiResponse, + FollowingEventsApiResponse, + EventCommentsApiResponse, +} from './center'; diff --git a/src/views/Center/Center.tsx b/src/views/Center/Center.tsx new file mode 100644 index 00000000..345c87c4 --- /dev/null +++ b/src/views/Center/Center.tsx @@ -0,0 +1,282 @@ +/** + * Center - 个人中心仪表板主页面 + * + * 对应路由:/home/center + * 功能:自选股监控、关注事件、投资规划等 + */ + +import React, { useEffect, useState, useCallback, useRef } from 'react'; +import { logger } from '@/utils/logger'; +import { getApiBase } from '@/utils/apiConfig'; +import { useDashboardEvents } from '@/hooks/useDashboardEvents'; +import { + Box, + Flex, + Text, + VStack, + useToast, + Spinner, + Center, +} from '@chakra-ui/react'; +import { useCenterColors } from './hooks'; +import { useAuth } from '@/contexts/AuthContext'; +import { useLocation, useNavigate } from 'react-router-dom'; +import InvestmentPlanningCenter from './components/InvestmentPlanningCenter'; +import { getEventDetailUrl } from '@/utils/idEncoder'; +import MarketDashboard from '@views/Profile/components/MarketDashboard'; +import StrategyCenter from '@views/Profile/components/StrategyCenter'; +import ForumCenter from '@views/Profile/components/ForumCenter'; +import WatchSidebar from '@views/Profile/components/WatchSidebar'; +import { THEME } from '@views/Profile/components/MarketDashboard/constants'; + +import type { + WatchlistItem, + RealtimeQuotesMap, + FollowingEvent, + EventComment, + WatchlistApiResponse, + RealtimeQuotesApiResponse, + FollowingEventsApiResponse, + EventCommentsApiResponse, + DashboardEventsResult, +} from '@/types'; + +/** + * CenterDashboard 组件 + * 个人中心仪表板主页面 + */ +const CenterDashboard: React.FC = () => { + const { user } = useAuth(); + const location = useLocation(); + const navigate = useNavigate(); + const toast = useToast(); + + // 提取 userId 为独立变量(优化依赖项) + const userId = user?.id; + + // 初始化 Dashboard 埋点 Hook(类型断言为 DashboardEventsResult) + const dashboardEvents = useDashboardEvents({ + pageType: 'center', + navigate + }) as DashboardEventsResult; + + // 颜色主题(使用 useCenterColors 封装,避免 7 次 useColorModeValue 调用) + const { secondaryText } = useCenterColors(); + + // 数据状态 + const [watchlist, setWatchlist] = useState([]); + const [realtimeQuotes, setRealtimeQuotes] = useState({}); + const [followingEvents, setFollowingEvents] = useState([]); + const [eventComments, setEventComments] = useState([]); + const [loading, setLoading] = useState(true); + const [quotesLoading, setQuotesLoading] = useState(false); + + // 使用 ref 跟踪是否已经加载过数据(首次加载标记) + const hasLoadedRef = useRef(false); + + /** + * 加载实时行情 + */ + const loadRealtimeQuotes = useCallback(async (): Promise => { + try { + setQuotesLoading(true); + const base = getApiBase(); + const response = await fetch(base + '/api/account/watchlist/realtime', { + credentials: 'include', + cache: 'no-store' + }); + + if (response.ok) { + const data: RealtimeQuotesApiResponse = await response.json(); + if (data.success) { + const quotesMap: RealtimeQuotesMap = {}; + data.data.forEach(item => { + quotesMap[item.stock_code] = item; + }); + setRealtimeQuotes(quotesMap); + } + } + } catch (error) { + logger.error('Center', 'loadRealtimeQuotes', error, { + userId, + timestamp: new Date().toISOString() + }); + } finally { + setQuotesLoading(false); + } + }, [userId]); + + /** + * 加载所有数据(自选股、关注事件、评论) + */ + const loadData = useCallback(async (): Promise => { + try { + const base = getApiBase(); + const ts = Date.now(); + + const [w, e, c] = await Promise.all([ + fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store' }), + fetch(base + `/api/account/events/following?_=${ts}`, { credentials: 'include', cache: 'no-store' }), + fetch(base + `/api/account/events/posts?_=${ts}`, { credentials: 'include', cache: 'no-store' }), + ]); + + const jw: WatchlistApiResponse = await w.json(); + const je: FollowingEventsApiResponse = await e.json(); + const jc: EventCommentsApiResponse = await c.json(); + + if (jw.success) { + const watchlistData = Array.isArray(jw.data) ? jw.data : []; + setWatchlist(watchlistData); + + // 追踪自选股列表查看 + if (watchlistData.length > 0) { + dashboardEvents.trackWatchlistViewed(watchlistData.length, true); + } + + // 加载实时行情 + if (jw.data && jw.data.length > 0) { + loadRealtimeQuotes(); + } + } + + if (je.success) { + const eventsData = Array.isArray(je.data) ? je.data : []; + setFollowingEvents(eventsData); + + // 追踪关注的事件列表查看 + dashboardEvents.trackFollowingEventsViewed(eventsData.length); + } + + if (jc.success) { + const commentsData = Array.isArray(jc.data) ? jc.data : []; + setEventComments(commentsData); + + // 追踪评论列表查看 + dashboardEvents.trackCommentsViewed(commentsData.length); + } + } catch (err) { + logger.error('Center', 'loadData', err, { + userId, + timestamp: new Date().toISOString() + }); + } finally { + setLoading(false); + } + }, [userId, loadRealtimeQuotes, dashboardEvents]); + + // 首次加载和页面可见性变化时加载数据 + useEffect(() => { + const isOnCenterPage = location.pathname.includes('/home/center'); + + // 首次进入页面且有用户时加载数据 + if (user && isOnCenterPage && !hasLoadedRef.current) { + console.log('[Center] 🚀 首次加载数据'); + hasLoadedRef.current = true; + loadData(); + } + + const onVis = (): void => { + if (document.visibilityState === 'visible' && location.pathname.includes('/home/center')) { + console.log('[Center] 👁️ visibilitychange 触发 loadData'); + loadData(); + } + }; + + document.addEventListener('visibilitychange', onVis); + return () => document.removeEventListener('visibilitychange', onVis); + }, [userId, location.pathname, loadData, user]); + + // 当用户登出再登入(userId 变化)时,重置加载标记 + useEffect(() => { + if (!user) { + hasLoadedRef.current = false; + } + }, [user]); + + // 定时刷新实时行情(每分钟一次) + useEffect(() => { + if (watchlist.length > 0) { + const interval = setInterval(() => { + loadRealtimeQuotes(); + }, 60000); // 60秒刷新一次 + + return () => clearInterval(interval); + } + }, [watchlist.length, loadRealtimeQuotes]); + + // 渲染加载状态 + if (loading) { + return ( +
+ + + 加载个人中心数据... + +
+ ); + } + + return ( + + + {/* 左右布局:左侧自适应,右侧固定200px */} + + {/* 左侧主内容区 */} + + {/* 市场概览仪表盘 */} + + + + + {/* 投资规划中心 */} + + + + + {/* 价值论坛 / 互动中心 */} + + + + + {/* 投资规划中心(整合了日历、计划、复盘) */} + + + + + + {/* 右侧固定宽度侧边栏 */} + + navigate(`/company/${stock.stock_code}`)} + onEventClick={(event: FollowingEvent) => navigate(getEventDetailUrl(event.id))} + onAddStock={() => navigate('/stocks')} + onAddEvent={() => navigate('/community')} + /> + + + + + ); +}; + +export default CenterDashboard; diff --git a/src/views/Dashboard/components/CalendarPanel.tsx b/src/views/Center/components/CalendarPanel.tsx similarity index 100% rename from src/views/Dashboard/components/CalendarPanel.tsx rename to src/views/Center/components/CalendarPanel.tsx diff --git a/src/views/Dashboard/components/EventCard.tsx b/src/views/Center/components/EventCard.tsx similarity index 100% rename from src/views/Dashboard/components/EventCard.tsx rename to src/views/Center/components/EventCard.tsx diff --git a/src/views/Dashboard/components/EventDetailModal.tsx b/src/views/Center/components/EventDetailModal.tsx similarity index 100% rename from src/views/Dashboard/components/EventDetailModal.tsx rename to src/views/Center/components/EventDetailModal.tsx diff --git a/src/views/Dashboard/components/EventEmptyState.tsx b/src/views/Center/components/EventEmptyState.tsx similarity index 100% rename from src/views/Dashboard/components/EventEmptyState.tsx rename to src/views/Center/components/EventEmptyState.tsx diff --git a/src/views/Dashboard/components/EventFormModal.less b/src/views/Center/components/EventFormModal.less similarity index 100% rename from src/views/Dashboard/components/EventFormModal.less rename to src/views/Center/components/EventFormModal.less diff --git a/src/views/Dashboard/components/EventFormModal.tsx b/src/views/Center/components/EventFormModal.tsx similarity index 100% rename from src/views/Dashboard/components/EventFormModal.tsx rename to src/views/Center/components/EventFormModal.tsx diff --git a/src/views/Dashboard/components/EventPanel.tsx b/src/views/Center/components/EventPanel.tsx similarity index 100% rename from src/views/Dashboard/components/EventPanel.tsx rename to src/views/Center/components/EventPanel.tsx diff --git a/src/views/Dashboard/components/InvestmentCalendar.less b/src/views/Center/components/InvestmentCalendar.less similarity index 100% rename from src/views/Dashboard/components/InvestmentCalendar.less rename to src/views/Center/components/InvestmentCalendar.less diff --git a/src/views/Dashboard/components/InvestmentPlanningCenter.tsx b/src/views/Center/components/InvestmentPlanningCenter.tsx similarity index 87% rename from src/views/Dashboard/components/InvestmentPlanningCenter.tsx rename to src/views/Center/components/InvestmentPlanningCenter.tsx index 06899ff7..92402ac1 100644 --- a/src/views/Dashboard/components/InvestmentPlanningCenter.tsx +++ b/src/views/Center/components/InvestmentPlanningCenter.tsx @@ -12,7 +12,7 @@ * - PlanningContext (数据共享层) */ -import React, { useState, useEffect, useCallback, Suspense, lazy } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react'; import { Box, Card, @@ -119,27 +119,46 @@ const InvestmentPlanningCenter: React.FC = () => { loadAllData(); }, [loadAllData]); - // 提供给子组件的 Context 值 - const contextValue: PlanningContextValue = { - allEvents, - setAllEvents, - loadAllData, - loading, - setLoading, - openPlanModalTrigger, - openReviewModalTrigger, - toast, - borderColor, - textColor, - secondaryText, - cardBg, - setViewMode, - setListTab, - }; + // 提供给子组件的 Context 值(使用 useMemo 缓存,避免子组件不必要的重渲染) + const contextValue: PlanningContextValue = useMemo( + () => ({ + allEvents, + setAllEvents, + loadAllData, + loading, + setLoading, + openPlanModalTrigger, + openReviewModalTrigger, + toast, + borderColor, + textColor, + secondaryText, + cardBg, + setViewMode, + setListTab, + }), + [ + allEvents, + loadAllData, + loading, + openPlanModalTrigger, + openReviewModalTrigger, + toast, + borderColor, + textColor, + secondaryText, + cardBg, + ] + ); - // 计算各类型事件数量 - const planCount = allEvents.filter(e => e.type === 'plan').length; - const reviewCount = allEvents.filter(e => e.type === 'review').length; + // 计算各类型事件数量(使用 useMemo 缓存,避免每次渲染重复遍历数组) + const { planCount, reviewCount } = useMemo( + () => ({ + planCount: allEvents.filter(e => e.type === 'plan').length, + reviewCount: allEvents.filter(e => e.type === 'review').length, + }), + [allEvents] + ); return ( diff --git a/src/views/Dashboard/components/MyFutureEvents.js b/src/views/Center/components/MyFutureEvents.js similarity index 100% rename from src/views/Dashboard/components/MyFutureEvents.js rename to src/views/Center/components/MyFutureEvents.js diff --git a/src/views/Dashboard/components/PlanningContext.tsx b/src/views/Center/components/PlanningContext.tsx similarity index 100% rename from src/views/Dashboard/components/PlanningContext.tsx rename to src/views/Center/components/PlanningContext.tsx diff --git a/src/views/Center/hooks/index.ts b/src/views/Center/hooks/index.ts new file mode 100644 index 00000000..7dfe76df --- /dev/null +++ b/src/views/Center/hooks/index.ts @@ -0,0 +1,5 @@ +/** + * Center 模块 Hooks 导出 + */ + +export { useCenterColors, default as useCenterColorsDefault } from './useCenterColors'; diff --git a/src/views/Center/hooks/useCenterColors.ts b/src/views/Center/hooks/useCenterColors.ts new file mode 100644 index 00000000..db2d0edf --- /dev/null +++ b/src/views/Center/hooks/useCenterColors.ts @@ -0,0 +1,41 @@ +/** + * useCenterColors Hook + * + * 封装 Center 模块的所有颜色变量,避免每次渲染重复调用 useColorModeValue + * 将 7 次 hook 调用合并为 1 次 useMemo 计算 + */ + +import { useMemo } from 'react'; +import { useColorModeValue } from '@chakra-ui/react'; +import type { CenterColors } from '@/types/center'; + +/** + * 获取 Center 模块的颜色配置 + * 使用 useMemo 缓存结果,避免每次渲染重新计算 + */ +export function useCenterColors(): CenterColors { + // 获取当前主题模式下的基础颜色 + const textColor = useColorModeValue('gray.700', 'white'); + const borderColor = useColorModeValue('gray.200', 'gray.600'); + const bgColor = useColorModeValue('white', 'gray.800'); + const hoverBg = useColorModeValue('gray.50', 'gray.700'); + const secondaryText = useColorModeValue('gray.600', 'gray.400'); + const cardBg = useColorModeValue('white', 'gray.800'); + const sectionBg = useColorModeValue('gray.50', 'gray.900'); + + // 使用 useMemo 缓存颜色对象,只在颜色值变化时重新创建 + return useMemo( + () => ({ + textColor, + borderColor, + bgColor, + hoverBg, + secondaryText, + cardBg, + sectionBg, + }), + [textColor, borderColor, bgColor, hoverBg, secondaryText, cardBg, sectionBg] + ); +} + +export default useCenterColors; diff --git a/src/views/Center/index.js b/src/views/Center/index.js new file mode 100644 index 00000000..37dca6a2 --- /dev/null +++ b/src/views/Center/index.js @@ -0,0 +1,4 @@ +// src/views/Center/index.js +// 入口文件,导出 Center 组件 + +export { default } from './Center'; diff --git a/src/views/Center/utils/formatters.ts b/src/views/Center/utils/formatters.ts new file mode 100644 index 00000000..19c2af30 --- /dev/null +++ b/src/views/Center/utils/formatters.ts @@ -0,0 +1,87 @@ +/** + * Center 模块格式化工具函数 + * + * 这些是纯函数,提取到组件外部避免每次渲染重建 + */ + +/** + * 格式化相对时间(如 "5分钟前"、"3天前") + * @param dateString 日期字符串 + * @returns 格式化后的相对时间字符串 + */ +export function formatRelativeTime(dateString: string | null | undefined): string { + if (!dateString) return ''; + + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays < 1) { + const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); + if (diffHours < 1) { + const diffMinutes = Math.ceil(diffTime / (1000 * 60)); + return `${diffMinutes}分钟前`; + } + return `${diffHours}小时前`; + } else if (diffDays < 7) { + return `${diffDays}天前`; + } else { + return date.toLocaleDateString('zh-CN'); + } +} + +/** + * 格式化数字(如 10000 → "1w",1500 → "1.5k") + * @param num 数字 + * @returns 格式化后的字符串 + */ +export function formatCompactNumber(num: number | null | undefined): string { + if (!num) return '0'; + + if (num >= 10000) { + return (num / 10000).toFixed(1) + 'w'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'k'; + } + return num.toString(); +} + +/** + * 根据热度分数获取颜色 + * @param score 热度分数 (0-100) + * @returns Chakra UI 颜色名称 + */ +export function getHeatColor(score: number): string { + if (score >= 80) return 'red'; + if (score >= 60) return 'orange'; + if (score >= 40) return 'yellow'; + return 'green'; +} + +/** + * 根据涨跌幅获取颜色 + * @param changePercent 涨跌幅百分比 + * @returns 颜色值 + */ +export function getChangeColor(changePercent: number | null | undefined): string { + if (changePercent === null || changePercent === undefined) { + return 'rgba(255, 255, 255, 0.6)'; + } + if (changePercent > 0) return '#EF4444'; // 红色(涨) + if (changePercent < 0) return '#22C55E'; // 绿色(跌) + return 'rgba(255, 255, 255, 0.6)'; // 灰色(平) +} + +/** + * 格式化涨跌幅显示 + * @param changePercent 涨跌幅百分比 + * @returns 格式化后的字符串(如 "+5.23%") + */ +export function formatChangePercent(changePercent: number | null | undefined): string { + if (changePercent === null || changePercent === undefined) { + return '--'; + } + const prefix = changePercent > 0 ? '+' : ''; + return `${prefix}${Number(changePercent).toFixed(2)}%`; +} diff --git a/src/views/Center/utils/index.ts b/src/views/Center/utils/index.ts new file mode 100644 index 00000000..ebecc059 --- /dev/null +++ b/src/views/Center/utils/index.ts @@ -0,0 +1,11 @@ +/** + * Center 模块工具函数导出 + */ + +export { + formatRelativeTime, + formatCompactNumber, + getHeatColor, + getChangeColor, + formatChangePercent, +} from './formatters'; diff --git a/src/views/Dashboard/Center.js b/src/views/Dashboard/Center.js deleted file mode 100644 index 11386c2f..00000000 --- a/src/views/Dashboard/Center.js +++ /dev/null @@ -1,610 +0,0 @@ -// src/views/Dashboard/Center.js -import React, { useEffect, useState, useCallback } from 'react'; -import { logger } from '../../utils/logger'; -import { getApiBase } from '../../utils/apiConfig'; -import { useDashboardEvents } from '../../hooks/useDashboardEvents'; -import { - Box, - Flex, - Grid, - SimpleGrid, - Stack, - Text, - Badge, - Button, - VStack, - HStack, - Card, - CardHeader, - CardBody, - Heading, - useColorModeValue, - Icon, - IconButton, - Stat, - StatLabel, - StatNumber, - StatHelpText, - StatArrow, - Divider, - Tag, - TagLabel, - TagLeftIcon, - Wrap, - WrapItem, - Avatar, - Tooltip, - Progress, - useToast, - LinkBox, - LinkOverlay, - Spinner, - Center, - Image, -} from '@chakra-ui/react'; -import { useAuth } from '../../contexts/AuthContext'; -import { useLocation, useNavigate, Link } from 'react-router-dom'; -import { - FiTrendingUp, - FiEye, - FiMessageSquare, - FiThumbsUp, - FiClock, - FiCalendar, - FiRefreshCw, - FiTrash2, - FiExternalLink, - FiPlus, - FiBarChart2, - FiStar, - FiActivity, - FiAlertCircle, - FiUsers, -} from 'react-icons/fi'; -import MyFutureEvents from './components/MyFutureEvents'; -import InvestmentPlanningCenter from './components/InvestmentPlanningCenter'; -import { getEventDetailUrl } from '@/utils/idEncoder'; - -export default function CenterDashboard() { - const { user } = useAuth(); - const location = useLocation(); - const navigate = useNavigate(); - const toast = useToast(); - - // ⚡ 提取 userId 为独立变量 - const userId = user?.id; - - // 🎯 初始化Dashboard埋点Hook - const dashboardEvents = useDashboardEvents({ - pageType: 'center', - navigate - }); - - // 颜色主题 - const textColor = useColorModeValue('gray.700', 'white'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); - const bgColor = useColorModeValue('white', 'gray.800'); - const hoverBg = useColorModeValue('gray.50', 'gray.700'); - const secondaryText = useColorModeValue('gray.600', 'gray.400'); - const cardBg = useColorModeValue('white', 'gray.800'); - const sectionBg = useColorModeValue('gray.50', 'gray.900'); - - const [watchlist, setWatchlist] = useState([]); - const [realtimeQuotes, setRealtimeQuotes] = useState({}); - const [followingEvents, setFollowingEvents] = useState([]); - const [eventComments, setEventComments] = useState([]); - const [loading, setLoading] = useState(true); - const [quotesLoading, setQuotesLoading] = useState(false); - - const loadData = useCallback(async () => { - try { - const base = getApiBase(); - const ts = Date.now(); - - const [w, e, c] = await Promise.all([ - fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store' }), - fetch(base + `/api/account/events/following?_=${ts}`, { credentials: 'include', cache: 'no-store' }), - fetch(base + `/api/account/events/posts?_=${ts}`, { credentials: 'include', cache: 'no-store' }), - ]); - - const jw = await w.json(); - const je = await e.json(); - const jc = await c.json(); - if (jw.success) { - const watchlistData = Array.isArray(jw.data) ? jw.data : []; - setWatchlist(watchlistData); - - // 🎯 追踪自选股列表查看 - if (watchlistData.length > 0) { - dashboardEvents.trackWatchlistViewed(watchlistData.length, true); - } - - // 加载实时行情 - if (jw.data && jw.data.length > 0) { - loadRealtimeQuotes(); - } - } - if (je.success) { - const eventsData = Array.isArray(je.data) ? je.data : []; - setFollowingEvents(eventsData); - - // 🎯 追踪关注的事件列表查看 - dashboardEvents.trackFollowingEventsViewed(eventsData.length); - } - if (jc.success) { - const commentsData = Array.isArray(jc.data) ? jc.data : []; - setEventComments(commentsData); - - // 🎯 追踪评论列表查看 - dashboardEvents.trackCommentsViewed(commentsData.length); - } - } catch (err) { - logger.error('Center', 'loadData', err, { - userId, - timestamp: new Date().toISOString() - }); - } finally { - setLoading(false); - } - }, [userId]); // ⚡ 使用 userId 而不是 user?.id - - // 加载实时行情 - const loadRealtimeQuotes = useCallback(async () => { - try { - setQuotesLoading(true); - const base = getApiBase(); - const response = await fetch(base + '/api/account/watchlist/realtime', { - credentials: 'include', - cache: 'no-store' - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - const quotesMap = {}; - data.data.forEach(item => { - quotesMap[item.stock_code] = item; - }); - setRealtimeQuotes(quotesMap); - } - } - } catch (error) { - logger.error('Center', 'loadRealtimeQuotes', error, { - userId: user?.id, - watchlistLength: watchlist.length - }); - } finally { - setQuotesLoading(false); - } - }, []); - - // 格式化日期 - const formatDate = (dateString) => { - if (!dateString) return ''; - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now - date); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays < 1) { - const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); - if (diffHours < 1) { - const diffMinutes = Math.ceil(diffTime / (1000 * 60)); - return `${diffMinutes}分钟前`; - } - return `${diffHours}小时前`; - } else if (diffDays < 7) { - return `${diffDays}天前`; - } else { - return date.toLocaleDateString('zh-CN'); - } - }; - - // 格式化数字 - const formatNumber = (num) => { - if (!num) return '0'; - if (num >= 10000) { - return (num / 10000).toFixed(1) + 'w'; - } else if (num >= 1000) { - return (num / 1000).toFixed(1) + 'k'; - } - return num.toString(); - }; - - // 获取事件热度颜色 - const getHeatColor = (score) => { - if (score >= 80) return 'red'; - if (score >= 60) return 'orange'; - if (score >= 40) return 'yellow'; - return 'green'; - }; - - // 🔧 使用 ref 跟踪是否已经加载过数据(首次加载标记) - const hasLoadedRef = React.useRef(false); - - useEffect(() => { - const isOnCenterPage = location.pathname.includes('/home/center'); - - // 首次进入页面且有用户时加载数据 - if (user && isOnCenterPage && !hasLoadedRef.current) { - console.log('[Center] 🚀 首次加载数据'); - hasLoadedRef.current = true; - loadData(); - } - - const onVis = () => { - if (document.visibilityState === 'visible' && location.pathname.includes('/home/center')) { - console.log('[Center] 👁️ visibilitychange 触发 loadData'); - loadData(); - } - }; - document.addEventListener('visibilitychange', onVis); - return () => document.removeEventListener('visibilitychange', onVis); - }, [userId, location.pathname, loadData, user]); - - // 当用户登出再登入(userId 变化)时,重置加载标记 - useEffect(() => { - if (!user) { - hasLoadedRef.current = false; - } - }, [user]); - - // 定时刷新实时行情(每分钟一次) - useEffect(() => { - if (watchlist.length > 0) { - const interval = setInterval(() => { - loadRealtimeQuotes(); - }, 60000); // 60秒刷新一次 - - return () => clearInterval(interval); - } - }, [watchlist.length, loadRealtimeQuotes]); - - // 渲染加载状态 - if (loading) { - return ( -
- - - 加载个人中心数据... - -
- ); - } - - return ( - - - {/* 主要内容区域 */} - - {/* 左列:自选股票 */} - - - - - - - 自选股票 - - {watchlist.length} - - {quotesLoading && } - - } - variant="ghost" - size="sm" - onClick={() => navigate('/stocks')} - aria-label="添加自选股" - /> - - - - {watchlist.length === 0 ? ( -
- - - - 暂无自选股 - - - -
- ) : ( - - {watchlist.slice(0, 10).map((stock) => ( - - - - - - {stock.stock_name || stock.stock_code} - - - - - {stock.stock_code} - - {realtimeQuotes[stock.stock_code] ? ( - 0 ? 'red' : 'green'} - fontSize="xs" - > - {realtimeQuotes[stock.stock_code].change_percent > 0 ? '+' : ''} - {realtimeQuotes[stock.stock_code].change_percent.toFixed(2)}% - - ) : stock.change_percent ? ( - 0 ? 'red' : 'green'} - fontSize="xs" - > - {stock.change_percent > 0 ? '+' : ''} - {stock.change_percent}% - - ) : null} - - - - - {realtimeQuotes[stock.stock_code]?.current_price?.toFixed(2) || stock.current_price || '--'} - - - {realtimeQuotes[stock.stock_code]?.update_time || stock.industry || '未分类'} - - - - - ))} - {watchlist.length > 10 && ( - - )} - - )} -
-
-
- - {/* 中列:关注事件 */} - - {/* 关注事件 */} - - - - - - 关注事件 - - {followingEvents.length} - - - } - variant="ghost" - size="sm" - onClick={() => navigate('/community')} - aria-label="添加关注事件" - /> - - - - {followingEvents.length === 0 ? ( -
- - - - 暂无关注事件 - - - -
- ) : ( - - {followingEvents.slice(0, 5).map((event) => ( - - - - - {event.title} - - - - {/* 事件标签 */} - {event.tags && event.tags.length > 0 && ( - - {event.tags.slice(0, 3).map((tag, idx) => ( - - - {tag} - - - ))} - - )} - - {/* 事件统计 */} - - {event.related_avg_chg !== undefined && event.related_avg_chg !== null && ( - 0 ? 'red' : 'green'} - variant="subtle" - > - 平均超额 {event.related_avg_chg > 0 ? '+' : ''}{Number(event.related_avg_chg).toFixed(2)}% - - )} - - - {event.follower_count || 0} 关注 - - - - {/* 事件信息 */} - - - - {event.creator?.username || '系统'} - · - {formatDate(event.created_at)} - - {event.exceed_expectation_score && ( - 70 ? 'red' : 'orange'} - variant="solid" - fontSize="xs" - > - 超预期 {event.exceed_expectation_score} - - )} - - - - ))} - {followingEvents.length > 5 && ( - - )} - - )} -
-
- -
- - {/* 右列:我的评论 */} - - {/* 我的评论 */} - - - - - - 我的评论 - - {eventComments.length} - - - - - - {eventComments.length === 0 ? ( -
- - - - 暂无评论记录 - - - 参与事件讨论,分享您的观点 - - -
- ) : ( - - {eventComments.slice(0, 5).map((comment) => ( - - - - {comment.content} - - - - - {formatDate(comment.created_at)} - - {comment.event_title && ( - - - {comment.event_title} - - - )} - - - - ))} - {eventComments.length > 5 && ( - - 共 {eventComments.length} 条评论 - - )} - - )} -
-
-
-
- - {/* 投资规划中心(整合了日历、计划、复盘) */} - - - -
-
- ); -} - - diff --git a/src/views/Dashboard/Default.js b/src/views/Dashboard/Default.js deleted file mode 100755 index 81353909..00000000 --- a/src/views/Dashboard/Default.js +++ /dev/null @@ -1,1421 +0,0 @@ -/*! - -========================================================= -* Argon Dashboard Chakra PRO - v1.0.0 -========================================================= - -* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro -* Copyright 2022 Creative Tim (https://www.creative-tim.com/) - -* Designed and Coded by Simmmple & Creative Tim - -========================================================= - -* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -*/ - -// Chakra imports -import { - Avatar, - AvatarGroup, - Badge, - Box, - Button, - Checkbox, - Flex, - Grid, - Icon, - Image, - Input, - Progress, - SimpleGrid, - Spacer, - Stack, - Stat, - StatLabel, - StatNumber, - Table, - Tbody, - Td, - Text, - Th, - Thead, - Tr, - useColorMode, - useColorModeValue, -} from '@chakra-ui/react'; -import avatar10 from 'assets/img/avatars/avatar10.png'; -import avatar2 from 'assets/img/avatars/avatar2.png'; -import avatar3 from 'assets/img/avatars/avatar3.png'; -import avatar4 from 'assets/img/avatars/avatar4.png'; -import avatar5 from 'assets/img/avatars/avatar5.png'; -import handBg from 'assets/img/hand-background.png'; -import teamsImage from 'assets/img/teams-image.png'; -// Custom components -import Card from 'components/Card/Card.js'; -import CardBody from 'components/Card/CardBody.js'; -import CardHeader from 'components/Card/CardHeader.js'; -import LineChart from 'components/Charts/LineChart'; -import IconBox from 'components/Icons/IconBox'; -import { BitcoinLogo } from 'components/Icons/Icons'; -// Custom icons -import { - AdobexdLogo, - CartIcon, - DocumentIcon, - GlobeIcon, - JiraLogo, - RocketIcon, - SettingsIcon, - SlackLogo, - SpotifyLogo, - WalletIcon, -} from 'components/Icons/Icons.js'; -import { HSeparator } from 'components/Separator/Separator'; -import TablesReportsRow from 'components/Tables/TablesReportsRow'; -import TablesTableRow from 'components/Tables/TablesTableRow'; -import React from 'react'; -import { AiFillLike, AiOutlinePlus } from 'react-icons/ai'; -import { - FaChevronDown, - FaChevronUp, - FaCommentDots, - FaUser, -} from 'react-icons/fa'; -import { IoMdShareAlt } from 'react-icons/io'; -import { IoBulb } from 'react-icons/io5'; -import { RiArrowDropRightLine } from 'react-icons/ri'; -import { - lineChartDataDefault, - lineChartOptionsDefault, -} from 'variables/charts'; -import { tablesReportsData, tablesTableData } from 'variables/general'; - -export default function Default() { - // Chakra Color Mode - const iconBlue = useColorModeValue('blue.500', 'blue.500'); - const iconBoxInside = useColorModeValue('white', 'white'); - const textColor = useColorModeValue('gray.700', 'white'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); - const cardColor = useColorModeValue('gray.800', 'navy.800'); - const bgBox = useColorModeValue('gray.800', 'blue.500'); - - const { colorMode } = useColorMode(); - - return ( - - - - - - - - Today's Money - - - - $53,897 - - - - - - - - - - +3.48%{' '} - - Since last month - - - - - - - - - Today's Users - - - - $3,200 - - - - - - - - - - +5.2%{' '} - - Since last month - - - - - - - - - New Clients - - - - +2,503 - - - - - - - - - - -2.82%{' '} - - Since last month - - - - - - - - - Total Sales - - - - $173,000 - - - - - - - - - - +8.12%{' '} - - Since last month - - - - - - - - - - - - Get started with Argon - - - Start your development process with an innovative admin dashboard! - - - - - - - Sales Overview - - - - (+5%) more - {' '} - in 2022 - - - - - - - - - - Team Members - - - - - - - - Esthera Jackson - - - Online - - - - - - - - - - - Esthera Jackson - - - In meeting - - - - - - - - - - - Esthera Jackson - - - Offline - - - - - - - - - - - Esthera Jackson - - - Online - - - - - - - - - - - - - To Do List - - - - - - Call with Dave - - - 09:30 AM - - - - - - - - Brunch Meeting - - - 11:00 AM - - - - - - - - Argon Dashboard Launch - - - 02:00 PM - - - - - - - - Winter Hackaton - - - 11:30 AM - - - - - - - - - - - Progress Track - - - - - - - React Material Dashboard - - - - - - - - - Argon Design System - - - - - - - - - VueJs Now UI Kit PRO - - - - - - - - - Soft UI Dashboard - - - - - - - - - - - - - - - - - - - Esthera Jackson - - - 3 days ago - - - - - - - - - Personal profiles are the perfect way for you to grab their - attention and persuade recruiters to continue reading your CV - because you’re telling them from the off exactly why they should - hire you. - - - - - - - 1502 - - - - 36 - - - - 12 - - - - - - - - - - - and 30+ more - - - - - - - - - - - Michael Lewis - - - I always felt like I could do anything. That’s the main thing - people are controlled by! Thoughts- their perception of - themselves! - - - - - 3 likes - - - - 2 shares - - - - - - - - - - - Jessica Stones - - - Society has put up so many boundaries, so many limitations on - what’s right and wrong that it’s almost impossible to get a - pure thought out. It’s like a little kid, a little boy. - - - - - 10 likes - - - - 1 share - - - - - - - - - - - Anthony Joshua - - - It's all about work ! Great ideas mean nothing if they aren't - realised by hungry, desiring people. - - - - - 42 likes - - - - 6 shares - - - - - - - - - - - - - - - - - - - - - - - - - - - - {tablesReportsData - .filter((_, idx) => idx < 4) - .map((row, index, arr) => { - return ( - - ); - })} - -
- Author - - Function - - Review - - Employed - - Date - - Id -
-
-
- - - - - - ${' '} - - 3,300 - - - Your current balance - - - - +15%{' '} - - ($250) - - - - - - Orders: 60% - - - - - - Sales: 40% - - - - - - - - - - - - Active - - - - Address - - - 0yx8Wkasd8uWpa083Jj81qZhs923K21 - - - - - Name - - - John Snow - - - - - - - - - - - - - - - - - - - - - Sales by Country - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Country: - - Sales: - - Value: - - Bounce: -
- - 🇺🇸 - - United States - - - - - 2500 - - - - $214,000 - - - - 40,22% - -
- - 🇩🇪 - - Germany - - - - - 3900 - - - - $446,700 - - - - 19,22% - -
- - 🇬🇧 - - Great Britain - - - - - 1300 - - - - $121,900 - - - - 39,22% - -
- - 🇧🇷 - - Brasil - - - - - 920 - - - - $52,100 - - - {' '} - - 29,9% - -
-
-
-
-
- - - - - - - - - - - - - - - {tablesTableData.map((row, index, arr) => { - return ( - - - - ); - })} - -
- Author - - Function - - Status - - Employed -
-
-
- - - - Categories - - - - - - - - - - - - Devices - - - 250 in stock,{' '} - - 346+ sold - - - - - - - - - - - - - - - Tickets - - - 123 closed,{' '} - - 15 open - - - - - - - - - - - - - - - Error logs - - - 1 is active,{' '} - - 40 closed - - - - - - - - - - - - - - - Happy Users - - - - +430 - - - - - - - - - - - - - - - Tickets - - - 123 closed,{' '} - - 15 open - - - - - - - - - - -
-
- ); -} diff --git a/src/views/Dashboard/components/InvestmentCalendarChakra.js b/src/views/Dashboard/components/InvestmentCalendarChakra.js deleted file mode 100644 index 4f1a72e9..00000000 --- a/src/views/Dashboard/components/InvestmentCalendarChakra.js +++ /dev/null @@ -1,587 +0,0 @@ -// src/views/Dashboard/components/InvestmentCalendarChakra.js -import React, { useState, useEffect, useCallback } from 'react'; -import { - Box, - Card, - CardHeader, - CardBody, - Heading, - VStack, - HStack, - Text, - Button, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, - useDisclosure, - Badge, - IconButton, - Flex, - Grid, - useColorModeValue, - Divider, - Tooltip, - Icon, - Input, - FormControl, - FormLabel, - Textarea, - Select, - useToast, - Spinner, - Center, - Tag, - TagLabel, - TagLeftIcon, -} from '@chakra-ui/react'; -import { - FiCalendar, - FiClock, - FiStar, - FiTrendingUp, - FiPlus, - FiEdit2, - FiTrash2, - FiSave, - FiX, -} from 'react-icons/fi'; -import FullCalendar from '@fullcalendar/react'; -import dayGridPlugin from '@fullcalendar/daygrid'; -import interactionPlugin from '@fullcalendar/interaction'; -import dayjs from 'dayjs'; -import 'dayjs/locale/zh-cn'; -import { logger } from '../../../utils/logger'; -import { getApiBase } from '../../../utils/apiConfig'; -import TimelineChartModal from '../../../components/StockChart/TimelineChartModal'; -import KLineChartModal from '../../../components/StockChart/KLineChartModal'; -import './InvestmentCalendar.less'; - -dayjs.locale('zh-cn'); - -export default function InvestmentCalendarChakra() { - const { isOpen, onOpen, onClose } = useDisclosure(); - const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure(); - const { isOpen: isTimelineModalOpen, onOpen: onTimelineModalOpen, onClose: onTimelineModalClose } = useDisclosure(); - const { isOpen: isKLineModalOpen, onOpen: onKLineModalOpen, onClose: onKLineModalClose } = useDisclosure(); - const toast = useToast(); - - // 颜色主题 - const bgColor = useColorModeValue('white', 'gray.800'); - const borderColor = useColorModeValue('gray.200', 'gray.600'); - const textColor = useColorModeValue('gray.700', 'white'); - const secondaryText = useColorModeValue('gray.600', 'gray.400'); - - const [events, setEvents] = useState([]); - const [selectedDate, setSelectedDate] = useState(null); - const [selectedDateEvents, setSelectedDateEvents] = useState([]); - const [selectedStock, setSelectedStock] = useState(null); - const [loading, setLoading] = useState(false); - const [newEvent, setNewEvent] = useState({ - title: '', - description: '', - type: 'plan', - importance: 3, - stocks: '', - }); - - // 加载事件数据 - const loadEvents = useCallback(async () => { - try { - setLoading(true); - const base = getApiBase(); - - // 直接加载用户相关的事件(投资计划 + 关注的未来事件) - const userResponse = await fetch(base + '/api/account/calendar/events', { - credentials: 'include' - }); - - if (userResponse.ok) { - const userData = await userResponse.json(); - if (userData.success) { - const allEvents = (userData.data || []).map(event => ({ - ...event, - id: `${event.source || 'user'}-${event.id}`, - title: event.title, - start: event.event_date, - date: event.event_date, - backgroundColor: event.source === 'future' ? '#3182CE' : '#8B5CF6', - borderColor: event.source === 'future' ? '#3182CE' : '#8B5CF6', - extendedProps: { - ...event, - isSystem: event.source === 'future', - } - })); - - setEvents(allEvents); - logger.debug('InvestmentCalendar', '日历事件加载成功', { - count: allEvents.length - }); - } - } - } catch (error) { - logger.error('InvestmentCalendar', 'loadEvents', error); - // ❌ 移除数据加载失败 toast(非关键操作) - } finally { - setLoading(false); - } - }, []); // ✅ 移除 toast 依赖 - - useEffect(() => { - loadEvents(); - }, [loadEvents]); - - // 根据重要性获取颜色 - const getEventColor = (importance) => { - if (importance >= 5) return '#E53E3E'; // 红色 - if (importance >= 4) return '#ED8936'; // 橙色 - if (importance >= 3) return '#ECC94B'; // 黄色 - if (importance >= 2) return '#48BB78'; // 绿色 - return '#3182CE'; // 蓝色 - }; - - // 处理日期点击 - const handleDateClick = (info) => { - const clickedDate = dayjs(info.date); - setSelectedDate(clickedDate); - - // 筛选当天的事件 - const dayEvents = events.filter(event => - dayjs(event.start).isSame(clickedDate, 'day') - ); - setSelectedDateEvents(dayEvents); - onOpen(); - }; - - // 处理事件点击 - const handleEventClick = (info) => { - const event = info.event; - const clickedDate = dayjs(event.start); - setSelectedDate(clickedDate); - setSelectedDateEvents([{ - title: event.title, - start: event.start, - extendedProps: { - ...event.extendedProps, - }, - }]); - onOpen(); - }; - - // 添加新事件 - const handleAddEvent = async () => { - try { - const base = getApiBase(); - - const eventData = { - ...newEvent, - event_date: (selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD')), - stocks: newEvent.stocks.split(',').map(s => s.trim()).filter(s => s), - }; - - const response = await fetch(base + '/api/account/calendar/events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify(eventData), - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - logger.info('InvestmentCalendar', '添加事件成功', { - eventTitle: eventData.title, - eventDate: eventData.event_date - }); - toast({ - title: '添加成功', - description: '投资计划已添加', - status: 'success', - duration: 3000, - }); - onAddClose(); - loadEvents(); - setNewEvent({ - title: '', - description: '', - type: 'plan', - importance: 3, - stocks: '', - }); - } - } - } catch (error) { - logger.error('InvestmentCalendar', 'handleAddEvent', error, { - eventTitle: newEvent?.title - }); - toast({ - title: '添加失败', - description: '无法添加投资计划', - status: 'error', - duration: 3000, - }); - } - }; - - // 删除用户事件 - const handleDeleteEvent = async (eventId) => { - if (!eventId) { - logger.warn('InvestmentCalendar', '删除事件失败', '缺少事件 ID', { eventId }); - toast({ - title: '无法删除', - description: '缺少事件 ID', - status: 'error', - duration: 3000, - }); - return; - } - try { - const base = getApiBase(); - - const response = await fetch(base + `/api/account/calendar/events/${eventId}`, { - method: 'DELETE', - credentials: 'include', - }); - - if (response.ok) { - logger.info('InvestmentCalendar', '删除事件成功', { eventId }); - toast({ - title: '删除成功', - status: 'success', - duration: 2000, - }); - loadEvents(); - } - } catch (error) { - logger.error('InvestmentCalendar', 'handleDeleteEvent', error, { eventId }); - toast({ - title: '删除失败', - status: 'error', - duration: 3000, - }); - } - }; - - // 处理股票点击 - 打开图表弹窗 - const handleStockClick = (stockCodeOrName, eventDate) => { - // 解析股票代码(可能是 "600000" 或 "600000 平安银行" 格式) - let stockCode = stockCodeOrName; - let stockName = ''; - - if (typeof stockCodeOrName === 'string') { - const parts = stockCodeOrName.trim().split(/\s+/); - stockCode = parts[0]; - stockName = parts.slice(1).join(' '); - } - - // 添加交易所后缀(如果没有) - if (!stockCode.includes('.')) { - if (stockCode.startsWith('6')) { - stockCode = `${stockCode}.SH`; - } else if (stockCode.startsWith('0') || stockCode.startsWith('3')) { - stockCode = `${stockCode}.SZ`; - } else if (stockCode.startsWith('8') || stockCode.startsWith('9') || stockCode.startsWith('4')) { - // 北交所股票 - stockCode = `${stockCode}.BJ`; - } - } - - setSelectedStock({ - stock_code: stockCode, - stock_name: stockName || stockCode, - }); - }; - - return ( - - - - - - 投资日历 - - - - - - {loading ? ( -
- -
- ) : ( - - - - )} -
- - {/* 查看事件详情 Modal - 条件渲染 */} - {isOpen && ( - - - - - {selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件 - - - - {selectedDateEvents.length === 0 ? ( -
- - 当天没有事件 - - -
- ) : ( - - {selectedDateEvents.map((event, idx) => ( - - - - - - {event.title} - - {event.extendedProps?.isSystem ? ( - 系统事件 - ) : ( - 我的计划 - )} - - - - - 重要度: {event.extendedProps?.importance || 3}/5 - - - - {!event.extendedProps?.isSystem && ( - } - size="sm" - variant="ghost" - colorScheme="red" - onClick={() => handleDeleteEvent(event.extendedProps?.id)} - /> - )} - - - {event.extendedProps?.description && ( - - {event.extendedProps.description} - - )} - - {event.extendedProps?.stocks && event.extendedProps.stocks.length > 0 && ( - - - 相关股票: - {event.extendedProps.stocks.map((stock, i) => ( - handleStockClick(stock, event.start)} - _hover={{ transform: 'scale(1.05)', shadow: 'md' }} - transition="all 0.2s" - > - - {stock} - - ))} - - {selectedStock && ( - - - - - )} - - )} - - ))} - - )} -
- - - -
-
- )} - - {/* 添加投资计划 Modal - 条件渲染 */} - {isAddOpen && ( - - - - - 添加投资计划 - - - - - - 标题 - setNewEvent({ ...newEvent, title: e.target.value })} - placeholder="例如:关注半导体板块" - /> - - - - 描述 -