diff --git a/src/mocks/handlers/account.js b/src/mocks/handlers/account.js index 8186ca45..d7bc881f 100644 --- a/src/mocks/handlers/account.js +++ b/src/mocks/handlers/account.js @@ -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 => diff --git a/src/store/slices/planningSlice.ts b/src/store/slices/planningSlice.ts index a9aa4629..e8d9b01f 100644 --- a/src/store/slices/planningSlice.ts +++ b/src/store/slices/planningSlice.ts @@ -196,6 +196,25 @@ const planningSlice = createSlice({ state.allEvents.push(action.payload); state.lastUpdated = Date.now(); }, + /** 乐观添加事件(插入到开头,使用临时 ID) */ + optimisticAddEvent: (state, action: PayloadAction) => { + 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) => { + 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; diff --git a/src/views/Center/components/EventFormModal.tsx b/src/views/Center/components/EventFormModal.tsx index 4b3192de..ed379ae0 100644 --- a/src/views/Center/components/EventFormModal.tsx +++ b/src/views/Center/components/EventFormModal.tsx @@ -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 = ({ setStockOptions(options.length > 0 ? options : watchlistOptions); }, [allStocks, watchlistOptions]); - // 保存数据 + // 保存数据(新建模式使用乐观更新) const handleSave = useCallback(async (): Promise => { try { const values = await form.validateFields(); @@ -316,23 +321,72 @@ export const EventFormModal: React.FC = ({ ? `${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 刷新数据,确保列表和日历同步