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 today = new Date().toISOString().split('T')[0];
|
||||||
const todayEvents = filteredEvents.filter(e =>
|
const todayEvents = filteredEvents.filter(e =>
|
||||||
|
|||||||
@@ -196,6 +196,25 @@ const planningSlice = createSlice({
|
|||||||
state.allEvents.push(action.payload);
|
state.allEvents.push(action.payload);
|
||||||
state.lastUpdated = Date.now();
|
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) => {
|
extraReducers: (builder) => {
|
||||||
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;
|
export default planningSlice.reducer;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,12 @@ import 'dayjs/locale/zh-cn';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useAppDispatch } from '@/store/hooks';
|
import { useAppDispatch } from '@/store/hooks';
|
||||||
import { fetchAllEvents } from '@/store/slices/planningSlice';
|
import {
|
||||||
|
fetchAllEvents,
|
||||||
|
optimisticAddEvent,
|
||||||
|
replaceEvent,
|
||||||
|
removeEvent,
|
||||||
|
} from '@/store/slices/planningSlice';
|
||||||
import './EventFormModal.less';
|
import './EventFormModal.less';
|
||||||
import type { InvestmentEvent, EventType } from '@/types';
|
import type { InvestmentEvent, EventType } from '@/types';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
@@ -276,7 +281,7 @@ export const EventFormModal: React.FC<EventFormModalProps> = ({
|
|||||||
setStockOptions(options.length > 0 ? options : watchlistOptions);
|
setStockOptions(options.length > 0 ? options : watchlistOptions);
|
||||||
}, [allStocks, watchlistOptions]);
|
}, [allStocks, watchlistOptions]);
|
||||||
|
|
||||||
// 保存数据
|
// 保存数据(新建模式使用乐观更新)
|
||||||
const handleSave = useCallback(async (): Promise<void> => {
|
const handleSave = useCallback(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
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}/${editingEvent.id}`
|
||||||
: `${base}/api/account/${apiEndpoint}`;
|
: `${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, {
|
const response = await fetch(url, {
|
||||||
method,
|
method: 'PUT',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify(requestData),
|
body: JSON.stringify(requestData),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
logger.info('EventFormModal', `${mode === 'edit' ? '更新' : '创建'}${label}成功`, {
|
logger.info('EventFormModal', `更新${label}成功`, {
|
||||||
itemId: editingEvent?.id,
|
itemId: editingEvent?.id,
|
||||||
title: values.title,
|
title: values.title,
|
||||||
});
|
});
|
||||||
message.success(mode === 'edit' ? '修改成功' : '添加成功');
|
message.success('修改成功');
|
||||||
onClose();
|
onClose();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
// 使用 Redux 刷新数据,确保列表和日历同步
|
// 使用 Redux 刷新数据,确保列表和日历同步
|
||||||
|
|||||||
Reference in New Issue
Block a user