refactor(Planning): 投资规划中心重构为 Redux 状态管理

- 新增 planningSlice 管理计划/复盘数据
- InvestmentPlanningCenter 改用 Redux 而非本地 state
- 列表和日历视图共享同一数据源,保持同步
- 优化 Mock handlers,改进事件 ID 生成和调试日志

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-23 14:15:49 +08:00
parent 0b683f4227
commit ab5b19847f
9 changed files with 554 additions and 104 deletions

View File

@@ -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<InvestmentEvent>, { 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<InvestmentEvent> }, { 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<InvestmentEvent[]>) => {
state.allEvents = action.payload;
state.lastUpdated = Date.now();
},
/** 添加单个事件(乐观更新) */
addEvent: (state, action: PayloadAction<InvestmentEvent>) => {
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');