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:
266
src/store/slices/planningSlice.ts
Normal file
266
src/store/slices/planningSlice.ts
Normal 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');
|
||||
Reference in New Issue
Block a user