From 9fd160efc5fa5f2b6ea8db041f6b0c92168d7898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 3 Sep 2026 14:37:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=89=B9=E9=87=8F=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=BB=91=E5=AE=9A=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批处理中的前置绑定批次跳过历史,最后一批使用非跳过替换统一形成撤销边界 为跳过后记录历史的状态基线补充撤销回归测试 --- .../features/ui-editor/useUiEditorState.ts | 17 +++++++- .../src/view/ui-editor/useUiEditorPage.ts | 4 +- .../tests/uiEditorState.test.ts | 43 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts b/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts index f87b140bf..a65ed50dc 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts @@ -563,6 +563,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { const isLockedRef = useRef(false); const undoStackRef = useRef>([]); const redoStackRef = useRef>([]); + const pendingHistoryBeforeRef = useRef(null); stateRef.current = state; const syncHistoryState = useCallback(() => { @@ -580,15 +581,20 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { const commit = useCallback( (nextState: State) => { const current = stateRef.current; - if (sameResource(current, nextState)) return false; + const before = pendingHistoryBeforeRef.current ?? current; + if (sameResource(before, nextState)) { + pendingHistoryBeforeRef.current = null; + return false; + } undoStackRef.current.push({ - before: cloneState(current), + before: cloneState(before), after: nextState, }); if (undoStackRef.current.length > MAX_HISTORY_LENGTH) { undoStackRef.current.shift(); } redoStackRef.current = []; + pendingHistoryBeforeRef.current = null; syncHistoryState(); applyState(nextState); return true; @@ -599,6 +605,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { const resetHistory = useCallback(() => { undoStackRef.current = []; redoStackRef.current = []; + pendingHistoryBeforeRef.current = null; syncHistoryState(); }, [syncHistoryState]); @@ -606,6 +613,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { if (isLockedRef.current) return false; const entry = undoStackRef.current.pop(); if (!entry) return false; + pendingHistoryBeforeRef.current = null; redoStackRef.current.push(entry); applyState(cloneState(entry.before)); syncHistoryState(); @@ -616,6 +624,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { if (isLockedRef.current) return false; const entry = redoStackRef.current.pop(); if (!entry) return false; + pendingHistoryBeforeRef.current = null; undoStackRef.current.push(entry); applyState(cloneState(entry.after)); syncHistoryState(); @@ -642,6 +651,7 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { try { return await operation(snapshot); } finally { + pendingHistoryBeforeRef.current = null; isLockedRef.current = false; setIsLocked(false); } @@ -1518,6 +1528,9 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { resetHistory(); applyState(next); } else if (options.history === 'skip') { + if (!pendingHistoryBeforeRef.current) { + pendingHistoryBeforeRef.current = cloneState(stateRef.current); + } applyState(next); } else { commit(next); diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts index e00d86176..b2c98484b 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts @@ -1028,7 +1028,9 @@ export function useUiEditorSession( spriteIds, }); current = applyBindingResult(current, result); - editor.replaceState(current); + editor.replaceState(current, { + history: index < batches.length - 1 ? 'skip' : 'record', + }); } setBindingStatus( `组件绑定完成(${batches.length}/${batches.length})。`, diff --git a/apps/ai-game-creator-shell/tests/uiEditorState.test.ts b/apps/ai-game-creator-shell/tests/uiEditorState.test.ts index df8e47bdd..89c4cdf3e 100644 --- a/apps/ai-game-creator-shell/tests/uiEditorState.test.ts +++ b/apps/ai-game-creator-shell/tests/uiEditorState.test.ts @@ -705,4 +705,47 @@ describe('useUiEditorState', () => { '中间', ); }); + + it('records one history entry after skipped replacement batches', () => { + const initial: State = { + ...structuredClone(EMPTY_UI_EDITOR_STATE), + ui_design_images: { page: image('Page') }, + }; + const { result } = renderHook(() => useUiEditorState(initial)); + + act(() => { + result.current.replaceState( + { + ...initial, + ui_design_images: { page: image('第一批') }, + }, + { history: 'skip' }, + ); + result.current.replaceState( + { + ...initial, + ui_design_images: { page: image('最终') }, + }, + { history: 'record' }, + ); + }); + + expect(result.current.state.ui_design_images.page?.metadata.name).toBe( + '最终', + ); + expect(result.current.historyState).toEqual({ + canUndo: true, + canRedo: false, + }); + act(() => { + expect(result.current.undo()).toBe(true); + }); + expect(result.current.state.ui_design_images.page?.metadata.name).toBe( + 'Page', + ); + expect(result.current.historyState).toEqual({ + canUndo: false, + canRedo: true, + }); + }); });