feat(Center): 投资规划中心新建计划/复盘乐观更新
- planningSlice: 添加 optimisticAddEvent、replaceEvent、removeEvent reducers - EventFormModal: 新建模式使用乐观更新,立即关闭弹窗显示数据 - account.js: Mock 数据按日期倒序排序,最新事件在前 乐观更新流程: 1. 创建临时事件(负数 ID)立即更新 UI 2. 后台发送 API 请求 3. 成功后替换为真实数据,失败则回滚 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -494,6 +494,13 @@ export const accountHandlers = [
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 按日期倒序排序(最新的在前面)
|
||||
filteredEvents.sort((a, b) => {
|
||||
const dateA = new Date(a.date || a.event_date);
|
||||
const dateB = new Date(b.date || b.event_date);
|
||||
return dateB - dateA; // 倒序:新日期在前
|
||||
});
|
||||
|
||||
// 打印今天的事件(方便调试)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const todayEvents = filteredEvents.filter(e =>
|
||||
|
||||
@@ -196,6 +196,25 @@ const planningSlice = createSlice({
|
||||
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();
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
@@ -251,7 +270,14 @@ const planningSlice = createSlice({
|
||||
|
||||
// ==================== 导出 ====================
|
||||
|
||||
export const { clearEvents, setEvents, addEvent } = planningSlice.actions;
|
||||
export const {
|
||||
clearEvents,
|
||||
setEvents,
|
||||
addEvent,
|
||||
optimisticAddEvent,
|
||||
replaceEvent,
|
||||
removeEvent,
|
||||
} = planningSlice.actions;
|
||||
|
||||
export default planningSlice.reducer;
|
||||
|
||||
|
||||
@@ -36,7 +36,12 @@ import 'dayjs/locale/zh-cn';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import { fetchAllEvents } from '@/store/slices/planningSlice';
|
||||
import {
|
||||
fetchAllEvents,
|
||||
optimisticAddEvent,
|
||||
replaceEvent,
|
||||
removeEvent,
|
||||
} from '@/store/slices/planningSlice';
|
||||
import './EventFormModal.less';
|
||||
import type { InvestmentEvent, EventType } from '@/types';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -276,7 +281,7 @@ export const EventFormModal: React.FC<EventFormModalProps> = ({
|
||||
setStockOptions(options.length > 0 ? options : watchlistOptions);
|
||||
}, [allStocks, watchlistOptions]);
|
||||
|
||||
// 保存数据
|
||||
// 保存数据(新建模式使用乐观更新)
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
@@ -316,23 +321,72 @@ export const EventFormModal: React.FC<EventFormModalProps> = ({
|
||||
? `${base}/api/account/${apiEndpoint}/${editingEvent.id}`
|
||||
: `${base}/api/account/${apiEndpoint}`;
|
||||
|
||||
const method = mode === 'edit' ? 'PUT' : 'POST';
|
||||
// ===== 新建模式:乐观更新 =====
|
||||
if (mode === 'create') {
|
||||
const tempId = -Date.now(); // 负数临时 ID,避免与服务器 ID 冲突
|
||||
const tempEvent: InvestmentEvent = {
|
||||
id: tempId,
|
||||
title: values.title,
|
||||
content: values.content || '',
|
||||
description: values.content || '',
|
||||
date: values.date.format('YYYY-MM-DD'),
|
||||
event_date: values.date.format('YYYY-MM-DD'),
|
||||
type: eventType,
|
||||
stocks: stocksWithNames,
|
||||
status: 'active',
|
||||
source: 'user',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// ① 立即更新 UI
|
||||
dispatch(optimisticAddEvent(tempEvent));
|
||||
setSaving(false);
|
||||
onClose(); // 立即关闭弹窗
|
||||
|
||||
// ② 后台发送 API 请求
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// ③ 用真实数据替换临时数据
|
||||
dispatch(replaceEvent({ tempId, realEvent: data.data }));
|
||||
logger.info('EventFormModal', `创建${label}成功`, { title: values.title });
|
||||
message.success('添加成功');
|
||||
onSuccess();
|
||||
} else {
|
||||
throw new Error(data.error || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
// ④ 失败回滚
|
||||
dispatch(removeEvent(tempId));
|
||||
logger.error('EventFormModal', 'handleSave optimistic rollback', error);
|
||||
message.error('创建失败,请重试');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 编辑模式:传统更新 =====
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logger.info('EventFormModal', `${mode === 'edit' ? '更新' : '创建'}${label}成功`, {
|
||||
logger.info('EventFormModal', `更新${label}成功`, {
|
||||
itemId: editingEvent?.id,
|
||||
title: values.title,
|
||||
});
|
||||
message.success(mode === 'edit' ? '修改成功' : '添加成功');
|
||||
message.success('修改成功');
|
||||
onClose();
|
||||
onSuccess();
|
||||
// 使用 Redux 刷新数据,确保列表和日历同步
|
||||
|
||||
Reference in New Issue
Block a user