/** * 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(); }, /** 乐观添加事件(插入到开头,使用临时 ID) */ optimisticAddEvent: (state, action: PayloadAction) => { state.allEvents.unshift(action.payload); // 新事件插入开头 state.lastUpdated = Date.now(); }, /** 替换临时事件为真实事件 */ replaceEvent: (state, action: PayloadAction<{ tempId: number; realEvent: InvestmentEvent }>) => { const { tempId, realEvent } = action.payload; const index = state.allEvents.findIndex(e => e.id === tempId); if (index !== -1) { state.allEvents[index] = realEvent; } state.lastUpdated = Date.now(); }, /** 移除事件(用于回滚) */ removeEvent: (state, action: PayloadAction) => { state.allEvents = state.allEvents.filter(e => e.id !== action.payload); state.lastUpdated = Date.now(); }, /** 乐观更新事件(编辑时使用) */ optimisticUpdateEvent: (state, action: PayloadAction) => { const index = state.allEvents.findIndex(e => e.id === action.payload.id); if (index !== -1) { state.allEvents[index] = 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, optimisticAddEvent, replaceEvent, removeEvent, optimisticUpdateEvent, } = 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');