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, + }); + }); });