diff --git a/src/mocks/data/account.js b/src/mocks/data/account.js index 9cd28bf7..ba694e60 100644 --- a/src/mocks/data/account.js +++ b/src/mocks/data/account.js @@ -770,6 +770,68 @@ export const mockInvestmentPlans = [ updated_at: '2024-10-08T10:00:00Z', tags: ['季度复盘', '半导体', 'Q3'], stocks: ['688981.SH', '002371.SZ'] + }, + + // ==================== 今日数据(用于日历视图展示) ==================== + { + id: 320, + user_id: 1, + type: 'plan', + title: '今日交易计划 - 年末布局', + content: `【今日目标】 +重点关注年末资金流向,寻找低位优质标的布局机会。 + +【操作计划】 +1. 白酒板块:观察茅台、五粮液走势,若出现回调可适当加仓 +2. 新能源:宁德时代逢低补仓,目标价位160元附近 +3. AI算力:关注寒武纪的突破信号 + +【资金安排】 +- 当前仓位:65% +- 可动用资金:35% +- 计划使用资金:15%(分3笔建仓) + +【风险控制】 +- 单笔止损:-3% +- 日内最大亏损:-5% +- 不追涨,只接回调`, + target_date: '2025-12-23', + status: 'active', + created_at: '2025-12-23T08:30:00Z', + updated_at: '2025-12-23T08:30:00Z', + tags: ['日计划', '年末布局'], + stocks: ['600519.SH', '300750.SZ', '688256.SH'] + }, + { + id: 321, + user_id: 1, + type: 'review', + title: '今日交易复盘 - 市场震荡', + content: `【操作回顾】 +1. 上午10:30 在茅台1580元位置加仓0.5% +2. 下午14:00 宁德时代触及160元支撑位,建仓1% +3. AI算力板块异动,寒武纪涨幅超5%,观望未操作 + +【盈亏分析】 +- 茅台加仓部分:浮盈+0.8% +- 宁德时代:浮亏-0.3%(正常波动范围内) +- 当日账户变动:+0.15% + +【经验总结】 +- 茅台买点把握较好,符合预期的回调位置 +- 宁德时代略显急躁,可以再等一等 +- AI算力虽然错过涨幅,但不追高的纪律执行到位 + +【明日计划】 +- 继续持有今日新增仓位 +- 如茅台继续上涨至1620,可考虑获利了结一半 +- 关注周五PMI数据公布对市场影响`, + target_date: '2025-12-23', + status: 'completed', + created_at: '2025-12-23T15:30:00Z', + updated_at: '2025-12-23T16:00:00Z', + tags: ['日复盘', '年末交易'], + stocks: ['600519.SH', '300750.SZ'] } ]; @@ -1101,6 +1163,87 @@ export const mockCalendarEvents = [ is_recurring: true, recurrence_rule: 'weekly', created_at: '2025-01-01T10:00:00Z' + }, + + // ==================== 今日事件(2025-12-23) ==================== + { + id: 409, + user_id: 1, + title: '比亚迪全球发布会', + date: '2025-12-23', + event_date: '2025-12-23', + type: 'earnings', + category: 'company_event', + description: `比亚迪将于今日14:00召开全球发布会,预计发布新一代刀片电池技术和2026年新车规划。 + +重点关注: +1. 刀片电池2.0技术参数:能量密度提升预期 +2. 2026年新车型规划:高端品牌仰望系列 +3. 海外市场扩张计划:欧洲建厂进度 +4. 年度交付量预告 + +投资建议: +- 关注发布会后股价走势 +- 若技术突破超预期,可考虑加仓 +- 设置止损位:当前价-5%`, + stock_code: '002594.SZ', + stock_name: '比亚迪', + importance: 5, + source: 'future', + stocks: ['002594.SZ', '300750.SZ', '601238.SH'], + created_at: '2025-12-20T10:00:00Z' + }, + { + id: 410, + user_id: 1, + title: '12月LPR报价公布', + date: '2025-12-23', + event_date: '2025-12-23', + type: 'policy', + category: 'macro_policy', + description: `中国人民银行将于今日9:30公布12月贷款市场报价利率(LPR)。 + +市场预期: +- 1年期LPR:3.10%(维持不变) +- 5年期以上LPR:3.60%(维持不变) + +影响板块: +1. 银行板块:利差压力关注 +2. 房地产:按揭成本影响 +3. 基建:融资成本变化 + +投资策略: +- 若降息,利好成长股,可加仓科技板块 +- 若维持,银行股防守价值凸显`, + importance: 5, + source: 'future', + stocks: ['601398.SH', '600036.SH', '000001.SZ'], + created_at: '2025-12-20T08:00:00Z' + }, + { + id: 411, + user_id: 1, + title: 'A股年末交易策略会议', + date: '2025-12-23', + event_date: '2025-12-23', + type: 'reminder', + category: 'personal', + description: `个人备忘:年末交易策略规划 + +待办事项: +1. 回顾2025年度投资收益 +2. 分析持仓股票基本面变化 +3. 制定2026年Q1布局计划 +4. 检查止盈止损纪律执行情况 + +重点关注: +- 白酒板块持仓是否需要调整 +- 新能源板块估值是否合理 +- 是否需要增加防守性配置`, + importance: 3, + source: 'user', + stocks: [], + created_at: '2025-12-22T20:00:00Z' } ]; diff --git a/src/mocks/handlers/account.js b/src/mocks/handlers/account.js index d151ae18..8186ca45 100644 --- a/src/mocks/handlers/account.js +++ b/src/mocks/handlers/account.js @@ -351,15 +351,21 @@ export const accountHandlers = [ const body = await request.json(); console.log('[Mock] 创建投资计划:', body); + // 生成唯一 ID(使用时间戳避免冲突) + const newId = Date.now(); + const newPlan = { - id: mockInvestmentPlans.length + 301, + id: newId, user_id: currentUser.id, ...body, + // 确保 target_date 字段存在(兼容前端发送的 date 字段) + target_date: body.target_date || body.date, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; mockInvestmentPlans.push(newPlan); + console.log('[Mock] 新增计划/复盘,当前总数:', mockInvestmentPlans.length); return HttpResponse.json({ success: true, @@ -488,13 +494,22 @@ export const accountHandlers = [ }); } + // 打印今天的事件(方便调试) + const today = new Date().toISOString().split('T')[0]; + const todayEvents = filteredEvents.filter(e => + (e.date === today || e.event_date === today) + ); + console.log('[Mock] 日历事件详情:', { currentUserId: currentUser.id, calendarEvents: calendarEvents.length, investmentPlansAsEvents: investmentPlansAsEvents.length, total: filteredEvents.length, plansCount: filteredEvents.filter(e => e.type === 'plan').length, - reviewsCount: filteredEvents.filter(e => e.type === 'review').length + reviewsCount: filteredEvents.filter(e => e.type === 'review').length, + today, + todayEventsCount: todayEvents.length, + todayEventTitles: todayEvents.map(e => `[${e.type}] ${e.title}`) }); return HttpResponse.json({ diff --git a/src/store/index.js b/src/store/index.js index 26a796b0..cbc59745 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -9,6 +9,7 @@ import stockReducer from './slices/stockSlice'; import authModalReducer from './slices/authModalSlice'; import subscriptionReducer from './slices/subscriptionSlice'; import deviceReducer from './slices/deviceSlice'; // ✅ 设备检测状态管理 +import planningReducer from './slices/planningSlice'; // ✅ 投资规划中心状态管理 import { eventsApi } from './api/eventsApi'; // ✅ RTK Query API // ⚡ 基础 reducers(首屏必需) @@ -19,6 +20,7 @@ const staticReducers = { authModal: authModalReducer, // ✅ 认证弹窗状态管理 subscription: subscriptionReducer, // ✅ 订阅信息状态管理 device: deviceReducer, // ✅ 设备检测状态管理(移动端/桌面端) + planning: planningReducer, // ✅ 投资规划中心状态管理 [eventsApi.reducerPath]: eventsApi.reducer, // ✅ RTK Query 事件 API }; diff --git a/src/store/slices/planningSlice.ts b/src/store/slices/planningSlice.ts new file mode 100644 index 00000000..a9aa4629 --- /dev/null +++ b/src/store/slices/planningSlice.ts @@ -0,0 +1,266 @@ +/** + * planningSlice - 投资规划中心 Redux Slice + * 管理计划、复盘和日历事件数据 + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import type { InvestmentEvent } from '@/types'; +import { getApiBase } from '@/utils/apiConfig'; +import { logger } from '@/utils/logger'; + +// ==================== State 类型定义 ==================== + +interface PlanningState { + /** 所有事件(计划 + 复盘 + 系统事件) */ + allEvents: InvestmentEvent[]; + /** 加载状态 */ + loading: boolean; + /** 错误信息 */ + error: string | null; + /** 最后更新时间 */ + lastUpdated: number | null; +} + +// ==================== 初始状态 ==================== + +const initialState: PlanningState = { + allEvents: [], + loading: false, + error: null, + lastUpdated: null, +}; + +// ==================== Async Thunks ==================== + +/** + * 加载所有事件数据 + */ +export const fetchAllEvents = createAsyncThunk( + 'planning/fetchAllEvents', + async (_, { rejectWithValue }) => { + try { + const base = getApiBase(); + const response = await fetch(`${base}/api/account/calendar/events`, { + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.success) { + logger.debug('planningSlice', '数据加载成功', { + count: data.data?.length || 0, + }); + return data.data || []; + } else { + throw new Error(data.error || '加载失败'); + } + } catch (error) { + logger.error('planningSlice', 'fetchAllEvents', error); + return rejectWithValue(error instanceof Error ? error.message : '加载失败'); + } + } +); + +/** + * 创建计划/复盘 + */ +export const createEvent = createAsyncThunk( + 'planning/createEvent', + async (eventData: Partial, { rejectWithValue, dispatch }) => { + try { + const base = getApiBase(); + const response = await fetch(`${base}/api/account/investment-plans`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(eventData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.success) { + logger.info('planningSlice', '创建成功', { title: eventData.title }); + // 创建成功后重新加载所有数据 + dispatch(fetchAllEvents()); + return data.data; + } else { + throw new Error(data.error || '创建失败'); + } + } catch (error) { + logger.error('planningSlice', 'createEvent', error); + return rejectWithValue(error instanceof Error ? error.message : '创建失败'); + } + } +); + +/** + * 更新计划/复盘 + */ +export const updateEvent = createAsyncThunk( + 'planning/updateEvent', + async ({ id, data }: { id: number; data: Partial }, { rejectWithValue, dispatch }) => { + try { + const base = getApiBase(); + const response = await fetch(`${base}/api/account/investment-plans/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + if (result.success) { + logger.info('planningSlice', '更新成功', { id }); + // 更新成功后重新加载所有数据 + dispatch(fetchAllEvents()); + return result.data; + } else { + throw new Error(result.error || '更新失败'); + } + } catch (error) { + logger.error('planningSlice', 'updateEvent', error); + return rejectWithValue(error instanceof Error ? error.message : '更新失败'); + } + } +); + +/** + * 删除计划/复盘 + */ +export const deleteEvent = createAsyncThunk( + 'planning/deleteEvent', + async (id: number, { rejectWithValue, dispatch }) => { + try { + const base = getApiBase(); + const response = await fetch(`${base}/api/account/investment-plans/${id}`, { + method: 'DELETE', + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.success) { + logger.info('planningSlice', '删除成功', { id }); + // 删除成功后重新加载所有数据 + dispatch(fetchAllEvents()); + return id; + } else { + throw new Error(data.error || '删除失败'); + } + } catch (error) { + logger.error('planningSlice', 'deleteEvent', error); + return rejectWithValue(error instanceof Error ? error.message : '删除失败'); + } + } +); + +// ==================== Slice ==================== + +const planningSlice = createSlice({ + name: 'planning', + initialState, + reducers: { + /** 清空数据 */ + clearEvents: (state) => { + state.allEvents = []; + state.error = null; + }, + /** 直接设置事件(用于乐观更新) */ + setEvents: (state, action: PayloadAction) => { + state.allEvents = action.payload; + state.lastUpdated = Date.now(); + }, + /** 添加单个事件(乐观更新) */ + addEvent: (state, action: PayloadAction) => { + state.allEvents.push(action.payload); + state.lastUpdated = Date.now(); + }, + }, + extraReducers: (builder) => { + builder + // fetchAllEvents + .addCase(fetchAllEvents.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchAllEvents.fulfilled, (state, action) => { + state.loading = false; + state.allEvents = action.payload; + state.lastUpdated = Date.now(); + }) + .addCase(fetchAllEvents.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }) + // createEvent + .addCase(createEvent.pending, (state) => { + state.loading = true; + }) + .addCase(createEvent.fulfilled, (state) => { + state.loading = false; + }) + .addCase(createEvent.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }) + // updateEvent + .addCase(updateEvent.pending, (state) => { + state.loading = true; + }) + .addCase(updateEvent.fulfilled, (state) => { + state.loading = false; + }) + .addCase(updateEvent.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }) + // deleteEvent + .addCase(deleteEvent.pending, (state) => { + state.loading = true; + }) + .addCase(deleteEvent.fulfilled, (state) => { + state.loading = false; + }) + .addCase(deleteEvent.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }); + }, +}); + +// ==================== 导出 ==================== + +export const { clearEvents, setEvents, addEvent } = planningSlice.actions; + +export default planningSlice.reducer; + +// ==================== Selectors ==================== + +export const selectAllEvents = (state: { planning: PlanningState }) => state.planning.allEvents; +export const selectPlanningLoading = (state: { planning: PlanningState }) => state.planning.loading; +export const selectPlanningError = (state: { planning: PlanningState }) => state.planning.error; +export const selectPlans = (state: { planning: PlanningState }) => + state.planning.allEvents.filter(e => e.type === 'plan' && e.source !== 'future'); +export const selectReviews = (state: { planning: PlanningState }) => + state.planning.allEvents.filter(e => e.type === 'review' && e.source !== 'future'); diff --git a/src/views/Center/components/CalendarPanel.tsx b/src/views/Center/components/CalendarPanel.tsx index 5a62531d..dbac0962 100644 --- a/src/views/Center/components/CalendarPanel.tsx +++ b/src/views/Center/components/CalendarPanel.tsx @@ -23,6 +23,8 @@ import type { EventClickArg } from '@fullcalendar/core'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/zh-cn'; +import { useAppSelector } from '@/store/hooks'; +import { selectAllEvents } from '@/store/slices/planningSlice'; import { usePlanningData } from './PlanningContext'; import { EventDetailModal } from './EventDetailModal'; import type { InvestmentEvent } from '@/types'; @@ -53,8 +55,11 @@ interface CalendarEvent { * 日历视图面板,显示所有投资事件 */ export const CalendarPanel: React.FC = () => { + // 从 Redux 获取数据(确保与列表视图同步) + const allEvents = useAppSelector(selectAllEvents); + + // UI 相关状态仍从 Context 获取 const { - allEvents, borderColor, secondaryText, setViewMode, @@ -143,11 +148,33 @@ export const CalendarPanel: React.FC = () => { opacity: '1 !important', }, }, + // 星期头(周日、周一等)- 金色 + '.fc-col-header-cell-cushion': { + color: '#D4AF37 !important', + fontWeight: '600 !important', + fontSize: '14px', + }, + // 日期数字 - 增强可见性 + '.fc-daygrid-day-number': { + color: 'rgba(255, 255, 255, 0.9) !important', + fontWeight: '600 !important', + fontSize: '14px !important', + padding: '4px 8px !important', + }, + // 非当前月份日期(灰色) + '.fc-day-other .fc-daygrid-day-number': { + color: 'rgba(255, 255, 255, 0.35) !important', + }, // 今天日期高亮边框 '.fc-daygrid-day.fc-day-today': { border: '2px solid #D4AF37 !important', backgroundColor: 'rgba(212, 175, 55, 0.1) !important', }, + // 今天日期数字 - 金色 + '.fc-daygrid-day.fc-day-today .fc-daygrid-day-number': { + color: '#D4AF37 !important', + fontWeight: '700 !important', + }, // 标题金色渐变 '.fc .fc-toolbar-title': { background: 'linear-gradient(135deg, #D4AF37 0%, #F5E6A3 100%)', diff --git a/src/views/Center/components/EventFormModal.tsx b/src/views/Center/components/EventFormModal.tsx index 46a16ac1..4b3192de 100644 --- a/src/views/Center/components/EventFormModal.tsx +++ b/src/views/Center/components/EventFormModal.tsx @@ -36,7 +36,7 @@ import 'dayjs/locale/zh-cn'; import { useSelector } from 'react-redux'; import { useAppDispatch } from '@/store/hooks'; -import { usePlanningData } from './PlanningContext'; +import { fetchAllEvents } from '@/store/slices/planningSlice'; import './EventFormModal.less'; import type { InvestmentEvent, EventType } from '@/types'; import { logger } from '@/utils/logger'; @@ -186,7 +186,6 @@ export const EventFormModal: React.FC = ({ label = '事件', apiEndpoint = 'investment-plans', }) => { - const { loadAllData } = usePlanningData(); const dispatch = useAppDispatch(); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); @@ -336,7 +335,8 @@ export const EventFormModal: React.FC = ({ message.success(mode === 'edit' ? '修改成功' : '添加成功'); onClose(); onSuccess(); - loadAllData(); + // 使用 Redux 刷新数据,确保列表和日历同步 + dispatch(fetchAllEvents()); } else { throw new Error('保存失败'); } @@ -352,7 +352,7 @@ export const EventFormModal: React.FC = ({ } finally { setSaving(false); } - }, [form, eventType, apiEndpoint, mode, editingEvent, label, onClose, onSuccess, loadAllData, allStocks, watchlist]); + }, [form, eventType, apiEndpoint, mode, editingEvent, label, onClose, onSuccess, dispatch, allStocks, watchlist]); // 监听键盘快捷键 Ctrl + Enter useEffect(() => { diff --git a/src/views/Center/components/EventPanel.tsx b/src/views/Center/components/EventPanel.tsx index ad0d123e..87199056 100644 --- a/src/views/Center/components/EventPanel.tsx +++ b/src/views/Center/components/EventPanel.tsx @@ -1,5 +1,5 @@ /** - * EventPanel - 通用事件面板组件 + * EventPanel - 通用事件面板组件 (Redux 版本) * 用于显示、编辑和管理投资计划或复盘 * * 通过 props 配置差异化行为: @@ -17,15 +17,22 @@ import { Spinner, Center, Icon, + useToast, } from '@chakra-ui/react'; import { FiFileText } from 'react-icons/fi'; -import { usePlanningData } from './PlanningContext'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { + fetchAllEvents, + deleteEvent, + selectPlans, + selectReviews, + selectPlanningLoading, +} from '@/store/slices/planningSlice'; import { EventFormModal } from './EventFormModal'; import { FUIEventCard } from './FUIEventCard'; import type { InvestmentEvent } from '@/types'; import { logger } from '@/utils/logger'; -import { getApiBase } from '@/utils/apiConfig'; /** * EventPanel Props @@ -51,15 +58,16 @@ export const EventPanel: React.FC = ({ label, openModalTrigger, }) => { - const { - allEvents, - loadAllData, - loading, - toast, - textColor, - secondaryText, - cardBg, - } = usePlanningData(); + const dispatch = useAppDispatch(); + const toast = useToast(); + + // Redux 状态 + const plans = useAppSelector(selectPlans); + const reviews = useAppSelector(selectReviews); + const loading = useAppSelector(selectPlanningLoading); + + // 根据类型选择事件列表 + const events = type === 'plan' ? plans : reviews; // 弹窗状态 const [isModalOpen, setIsModalOpen] = useState(false); @@ -69,9 +77,6 @@ export const EventPanel: React.FC = ({ // 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发 const prevTriggerRef = useRef(openModalTrigger || 0); - // 筛选事件列表(按类型过滤,排除系统事件) - const events = allEvents.filter(event => event.type === type && event.source !== 'future'); - // 监听外部触发打开新建模态框(修复 bug:只在值变化时触发) useEffect(() => { if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) { @@ -99,27 +104,18 @@ export const EventPanel: React.FC = ({ setEditingItem(null); }; - // 删除数据 + // 删除数据 - 使用 Redux action const handleDelete = async (id: number): Promise => { if (!window.confirm('确定要删除吗?')) return; try { - const base = getApiBase(); - - const response = await fetch(base + `/api/account/investment-plans/${id}`, { - method: 'DELETE', - credentials: 'include', + await dispatch(deleteEvent(id)).unwrap(); + logger.info('EventPanel', `删除${label}成功`, { itemId: id }); + toast({ + title: '删除成功', + status: 'success', + duration: 2000, }); - - if (response.ok) { - logger.info('EventPanel', `删除${label}成功`, { itemId: id }); - toast({ - title: '删除成功', - status: 'success', - duration: 2000, - }); - loadAllData(); - } } catch (error) { logger.error('EventPanel', 'handleDelete', error, { itemId: id }); toast({ @@ -130,11 +126,19 @@ export const EventPanel: React.FC = ({ } }; + // 刷新数据 + const handleRefresh = useCallback(() => { + dispatch(fetchAllEvents()); + }, [dispatch]); + // 使用 useCallback 优化回调函数 const handleEdit = useCallback((item: InvestmentEvent) => { handleOpenModal(item); }, []); + // 颜色主题 + const secondaryText = 'rgba(255, 255, 255, 0.6)'; + return ( @@ -172,7 +176,7 @@ export const EventPanel: React.FC = ({ mode={modalMode} eventType={type} editingEvent={editingItem} - onSuccess={loadAllData} + onSuccess={handleRefresh} label={label} apiEndpoint="investment-plans" /> diff --git a/src/views/Center/components/InvestmentCalendar.less b/src/views/Center/components/InvestmentCalendar.less index 1c32dbd8..069e4a84 100644 --- a/src/views/Center/components/InvestmentCalendar.less +++ b/src/views/Center/components/InvestmentCalendar.less @@ -124,22 +124,41 @@ } } -// 星期头 +// 星期头(周日、周一等) .fc-col-header-cell-cushion { - color: @text-secondary; - font-weight: 500; + color: @gold-400 !important; + font-weight: 600 !important; + font-size: 14px; } -// 日期数字 +// 日期数字(1日、2日等)- 增强可见性 .fc-daygrid-day-number { - color: @text-primary; - font-weight: 500; + color: rgba(255, 255, 255, 0.9) !important; + font-weight: 600 !important; + font-size: 14px !important; + padding: 4px 8px !important; +} + +// 当前月份的日期更亮 +.fc-day.fc-day-future .fc-daygrid-day-number, +.fc-day.fc-day-past .fc-daygrid-day-number { + color: rgba(255, 255, 255, 0.85) !important; +} + +// 非当前月份的日期(灰色) +.fc-day-other .fc-daygrid-day-number { + color: rgba(255, 255, 255, 0.35) !important; } // 今天高亮 .fc-daygrid-day.fc-day-today { background-color: @today-bg !important; border: 2px solid @gold-400 !important; + + .fc-daygrid-day-number { + color: @gold-400 !important; + font-weight: 700 !important; + } } // 事件样式 diff --git a/src/views/Center/components/InvestmentPlanningCenter.tsx b/src/views/Center/components/InvestmentPlanningCenter.tsx index 70ed3b53..e2b93c34 100644 --- a/src/views/Center/components/InvestmentPlanningCenter.tsx +++ b/src/views/Center/components/InvestmentPlanningCenter.tsx @@ -1,18 +1,16 @@ /** - * InvestmentPlanningCenter - 投资规划中心主组件 (TypeScript 重构版) + * InvestmentPlanningCenter - 投资规划中心主组件 (Redux 版本) * - * 性能优化: - * - 使用 React.lazy() 懒加载子面板,减少初始加载时间 - * - 使用 TypeScript 提供类型安全 + * 使用 Redux 管理数据,确保列表和日历视图数据同步 * * 组件架构: * - InvestmentPlanningCenter (主组件) * - CalendarPanel (日历面板,懒加载) * - EventPanel (通用事件面板,用于计划和复盘) - * - PlanningContext (数据共享层) + * - PlanningContext (UI 状态共享) */ -import React, { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react'; +import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react'; import { Box, Heading, @@ -30,7 +28,6 @@ import { Center, Button, ButtonGroup, - Text, } from '@chakra-ui/react'; import { FiCalendar, @@ -42,9 +39,15 @@ import { Target } from 'lucide-react'; import GlassCard from '@components/GlassCard'; import { PlanningDataProvider } from './PlanningContext'; -import type { InvestmentEvent, PlanningContextValue } from '@/types'; -import { logger } from '@/utils/logger'; -import { getApiBase } from '@/utils/apiConfig'; +import type { PlanningContextValue } from '@/types'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { + fetchAllEvents, + selectAllEvents, + selectPlanningLoading, + selectPlans, + selectReviews, +} from '@/store/slices/planningSlice'; import './InvestmentCalendar.less'; // 懒加载子面板组件(实现代码分割) @@ -68,64 +71,45 @@ const PanelLoadingFallback: React.FC = () => ( * InvestmentPlanningCenter 主组件 */ const InvestmentPlanningCenter: React.FC = () => { + const dispatch = useAppDispatch(); const toast = useToast(); + // Redux 状态 + const allEvents = useAppSelector(selectAllEvents); + const loading = useAppSelector(selectPlanningLoading); + const plans = useAppSelector(selectPlans); + const reviews = useAppSelector(selectReviews); + // 颜色主题 - 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 cardBg = useColorModeValue('gray.50', 'gray.700'); - // 全局数据状态 - const [allEvents, setAllEvents] = useState([]); - const [loading, setLoading] = useState(false); + // UI 状态 const [viewMode, setViewMode] = useState<'calendar' | 'list'>('list'); const [listTab, setListTab] = useState(0); // 0: 我的计划, 1: 我的复盘 const [openPlanModalTrigger, setOpenPlanModalTrigger] = useState(0); const [openReviewModalTrigger, setOpenReviewModalTrigger] = useState(0); - /** - * 加载所有事件数据(日历事件 + 计划 + 复盘) - */ - const loadAllData = useCallback(async (): Promise => { - try { - setLoading(true); - const base = getApiBase(); - - const response = await fetch(base + '/api/account/calendar/events', { - credentials: 'include' - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - setAllEvents(data.data || []); - logger.debug('InvestmentPlanningCenter', '数据加载成功', { - count: data.data?.length || 0 - }); - } - } - } catch (error) { - logger.error('InvestmentPlanningCenter', 'loadAllData', error); - } finally { - setLoading(false); - } - }, []); - // 组件挂载时加载数据 useEffect(() => { - loadAllData(); - }, [loadAllData]); + dispatch(fetchAllEvents()); + }, [dispatch]); - // 提供给子组件的 Context 值(使用 useMemo 缓存,避免子组件不必要的重渲染) + // 刷新数据的方法(供子组件调用) + const loadAllData = async (): Promise => { + await dispatch(fetchAllEvents()); + }; + + // 提供给子组件的 Context 值 const contextValue: PlanningContextValue = useMemo( () => ({ allEvents, - setAllEvents, + setAllEvents: () => {}, // Redux 管理,不需要 setter loadAllData, loading, - setLoading, + setLoading: () => {}, // Redux 管理,不需要 setter openPlanModalTrigger, openReviewModalTrigger, toast, @@ -138,7 +122,6 @@ const InvestmentPlanningCenter: React.FC = () => { }), [ allEvents, - loadAllData, loading, openPlanModalTrigger, openReviewModalTrigger, @@ -150,15 +133,6 @@ const InvestmentPlanningCenter: React.FC = () => { ] ); - // 计算各类型事件数量(使用 useMemo 缓存,避免每次渲染重复遍历数组) - const { planCount, reviewCount } = useMemo( - () => ({ - planCount: allEvents.filter(e => e.type === 'plan').length, - reviewCount: allEvents.filter(e => e.type === 'review').length, - }), - [allEvents] - ); - // 金色主题色 const goldAccent = 'rgba(212, 175, 55, 0.9)'; @@ -245,7 +219,7 @@ const InvestmentPlanningCenter: React.FC = () => { }} > - 我的计划 ({planCount}) + 我的计划 ({plans.length}) { }} > - 我的复盘 ({reviewCount}) + 我的复盘 ({reviews.length})