Files
vf_react/src/store/slices/planningSlice.ts

302 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();
},
/** 乐观添加事件(插入到开头,使用临时 ID */
optimisticAddEvent: (state, action: PayloadAction<InvestmentEvent>) => {
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<number>) => {
state.allEvents = state.allEvents.filter(e => e.id !== action.payload);
state.lastUpdated = Date.now();
},
/** 乐观更新事件(编辑时使用) */
optimisticUpdateEvent: (state, action: PayloadAction<InvestmentEvent>) => {
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');