接入 UI 编辑器撤销重做入口

在编辑器头部增加撤销与重做按钮及禁用态

增加桌面 Cmd/Ctrl 快捷键并保留文本控件原生行为
This commit is contained in:
2026-09-03 11:07:19 +08:00
parent 0544e78996
commit dc0ea0eda0
@@ -1,4 +1,4 @@
import { ChevronLeft } from 'lucide-react';
import { ChevronLeft, Redo2, Undo2 } from 'lucide-react';
import { type ReactNode, useEffect, useMemo, useState } from 'react';
import { ThemedModal } from '../../components/modal/ThemedModal';
@@ -82,6 +82,37 @@ export default function UiEditorPage({
if (session.save.isDirty) setGenerateSuccess(null);
}, [session.save.isDirty]);
useEffect(() => {
const isEditableTarget = (target: EventTarget | null) => {
const element = target instanceof HTMLElement ? target : null;
return Boolean(
element?.isContentEditable ||
element?.closest(
'input, textarea, select, button, a, [contenteditable="true"], [role="button"]',
),
);
};
const onKeyDown = (event: KeyboardEvent) => {
if (
isEditableTarget(event.target) ||
(!event.ctrlKey && !event.metaKey)
) {
return;
}
const isUndo = event.key.toLowerCase() === 'z' && !event.shiftKey;
const isRedo =
(event.key.toLowerCase() === 'z' && event.shiftKey) ||
(event.ctrlKey && event.key.toLowerCase() === 'y');
if (isUndo && session.history.undo()) {
event.preventDefault();
} else if (isRedo && session.history.redo()) {
event.preventDefault();
}
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [session.history]);
async function save(afterReturn = false) {
if (await session.save.save()) {
if (afterReturn) {
@@ -155,6 +186,26 @@ export default function UiEditorPage({
</button>
<strong className="text-sm">{resourceLabel ?? 'UI 设计'}</strong>
<div className="game-workbench-editor-actions">
<button
type="button"
className="inline-flex items-center gap-1 rounded-lg border border-(--platform-subpanel-border) px-2.5 py-1.5 text-sm disabled:cursor-not-allowed disabled:opacity-40"
aria-label="撤销"
disabled={!session.history.canUndo || session.save.isLocked}
onClick={() => session.history.undo()}
>
<Undo2 size={16} aria-hidden="true" />
</button>
<button
type="button"
className="inline-flex items-center gap-1 rounded-lg border border-(--platform-subpanel-border) px-2.5 py-1.5 text-sm disabled:cursor-not-allowed disabled:opacity-40"
aria-label="重做"
disabled={!session.history.canRedo || session.save.isLocked}
onClick={() => session.history.redo()}
>
<Redo2 size={16} aria-hidden="true" />
</button>
{walletEntry ? (
<div className="game-workbench-editor-wallet">{walletEntry}</div>
) : null}