接入 UI 编辑器保存结果弹窗

让普通保存和保存并生成流程返回结构化结果

将成功与失败反馈接入独立弹窗并保留保存并返回直返行为
This commit is contained in:
2026-09-14 18:44:23 +08:00
parent 43f3780a38
commit 27859d3e19
2 changed files with 87 additions and 45 deletions
@@ -16,6 +16,10 @@ import { PreviewWorkspace } from './components/preview/PreviewWorkspace';
import { RecognitionOverview } from './components/RecognitionOverview';
import { SeparationOverview } from './components/SeparationOverview';
import { ToolNavigation } from './components/ToolNavigation';
import {
UiEditorSaveResultModal,
type UiEditorSaveResultNotice,
} from './components/UiEditorSaveResultModal';
import { WorkflowActionCard } from './components/WorkflowActionCard';
import { WorkflowCompletionModal } from './components/WorkflowCompletionModal';
import { UI_EDITOR_STEPS, type UiEditorStepId } from './model';
@@ -70,7 +74,8 @@ export default function UiEditorPage({
const [saveWarningOpen, setSaveWarningOpen] = useState(false);
const [saveAfterReturn, setSaveAfterReturn] = useState(false);
const [generateAfterWarning, setGenerateAfterWarning] = useState(false);
const [generateSuccess, setGenerateSuccess] = useState<string | null>(null);
const [saveResultNotice, setSaveResultNotice] =
useState<UiEditorSaveResultNotice | null>(null);
const [returnConfirmOpen, setReturnConfirmOpen] = useState(false);
const [showFrame, setShowFrame] = useState(true);
const [showOriginImage, setShowOriginImage] = useState(true);
@@ -115,10 +120,6 @@ export default function UiEditorPage({
const activeImageId = session.input.activeImageId;
const deleteNode = session.input.deleteNode;
useEffect(() => {
if (session.save.isDirty) setGenerateSuccess(null);
}, [session.save.isDirty]);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) =>
handleUiEditorKeyDown(event, {
@@ -133,28 +134,51 @@ export default function UiEditorPage({
}, [activeImageId, deleteNode, historyRedo, historyUndo, selectedNodeId]);
async function save(afterReturn = false) {
if (await session.save.save()) {
const result = await session.save.save();
if (result.status === 'saved') {
if (afterReturn) {
setReturnConfirmOpen(false);
onBack?.();
} else {
setSaveResultNotice({ kind: 'saved' });
}
return;
}
setSaveResultNotice({
kind: 'failure',
message: result.message,
retryLabel: '重试保存',
onRetry: () => void save(afterReturn),
});
}
async function saveAndGenerate() {
setGenerateSuccess(null);
setSaveResultNotice(null);
const result = await session.save.saveAndGenerateCode();
if (result) {
if (result.status === 'generated') {
setGenerateAfterWarning(false);
setGenerateSuccess(`代码已生成:${result.relativePath}`);
setSaveResultNotice({
kind: 'generated',
relativePath: result.result.relativePath,
});
return;
}
setSaveResultNotice({
kind: 'failure',
message:
result.phase === 'generate'
? `项目已保存,但代码生成失败:${result.message}`
: result.message,
retryLabel: '重试保存并生成',
onRetry: () => void saveAndGenerate(),
});
}
function requestSave(afterReturn = false) {
if (!resourceId || session.save.isSaving || session.workflow.isAiRunning) {
return;
}
setGenerateSuccess(null);
setSaveResultNotice(null);
setSaveAfterReturn(afterReturn);
setGenerateAfterWarning(false);
if (session.save.hasWarnings()) {
@@ -174,7 +198,7 @@ export default function UiEditorPage({
return;
}
setSaveAfterReturn(false);
setGenerateSuccess(null);
setSaveResultNotice(null);
setGenerateAfterWarning(true);
if (session.save.hasWarnings()) {
setSaveWarningOpen(true);
@@ -289,30 +313,6 @@ export default function UiEditorPage({
<X size={18} aria-hidden="true" />
</button>
) : null}
{session.save.saveError ? (
<div
className="relative z-20 mx-4 mt-3 flex shrink-0 items-center justify-between gap-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800"
role="alert"
>
<span>{session.save.saveError}</span>
</div>
) : null}
{session.save.generateError ? (
<div
className="relative z-20 mx-4 mt-3 flex shrink-0 items-center justify-between gap-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800"
role="alert"
>
<span>{session.save.generateError}</span>
</div>
) : null}
{generateSuccess ? (
<div
className="relative z-20 mx-4 mt-3 shrink-0 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs text-emerald-800"
role="status"
>
{generateSuccess}
</div>
) : null}
<div className="absolute inset-0 min-h-0 min-w-0 overflow-hidden">
<PreviewWorkspace
canvas={session.canvas}
@@ -357,6 +357,10 @@ export default function UiEditorPage({
notice={session.workflow.completionNotice}
onClose={session.workflow.dismissCompletionNotice}
/>
<UiEditorSaveResultModal
notice={saveResultNotice}
onClose={() => setSaveResultNotice(null)}
/>
<ThemedModal
open={returnConfirmOpen}
onClose={() => setReturnConfirmOpen(false)}
@@ -37,6 +37,7 @@ import type { UIDesignSuggestionTreeNode } from '../../features/ui-editor/types/
import type { UiNodeMoveRequest } from '../../features/ui-editor/types/UiNodeMoveRequest';
import {
type IUiDesignStateStore,
type UiDesignCodeGenerationResult,
uiDesignStateStore,
} from '../../features/ui-editor/uiDesignStateStore';
import { applyUiDesignSuggestions } from '../../features/ui-editor/uiDesignSuggestions';
@@ -78,6 +79,14 @@ import { useUiEditorNodeFocus } from './useUiEditorNodeFocus';
const SEPARATION_IMPORT_BATCH_SIZE = 100;
export type UiEditorSaveResult =
| { status: 'saved' }
| { status: 'failed'; message: string };
export type UiEditorSaveAndGenerateResult =
| { status: 'generated'; result: UiDesignCodeGenerationResult }
| { status: 'failed'; phase: 'save' | 'generate'; message: string };
type LocalImageImportResponse = {
assets: Array<{ id: string; localPath: string; assetKind?: string | null }>;
};
@@ -1248,7 +1257,7 @@ export function useUiEditorSession(
if (separationResult === null)
throw new Error('自动切分素材没有返回结果');
const completedResult = separationResult as SeparationDTO;
if (!(await save({ allowDuringSeparation: true }))) {
if ((await save({ allowDuringSeparation: true })).status !== 'saved') {
throw new Error(
'自动切分素材结果已写入编辑器,但 State 保存失败;sidecar 已保留,可继续恢复。',
);
@@ -1411,7 +1420,10 @@ export function useUiEditorSession(
persistedRevision === null ||
editor.isLocked
) {
return false;
return {
status: 'failed' as const,
message: '当前状态不允许保存,请稍后重试。',
};
}
setSaveError(null);
setGenerateError(null);
@@ -1426,15 +1438,18 @@ export function useUiEditorSession(
);
if (result.status === 'conflict') {
setSaveError('资源已在别处更新;请重新加载后再保存。');
return false;
return {
status: 'failed' as const,
message: '资源已在别处更新;请重新加载后再保存。',
};
}
setPersistedRevision(result.revision);
setSavedStateSignature(snapshotSignature);
return true;
return { status: 'saved' as const };
});
} catch {
setSaveError('保存失败,请稍后重试。');
return false;
return { status: 'failed' as const, message: '保存失败,请稍后重试。' };
} finally {
setIsSaving(false);
}
@@ -1479,7 +1494,11 @@ export function useUiEditorSession(
persistedRevision === null ||
editor.isLocked
) {
return null;
return {
status: 'failed' as const,
phase: 'save' as const,
message: '当前状态不允许保存并生成代码,请稍后重试。',
};
}
setSaveError(null);
setGenerateError(null);
@@ -1495,15 +1514,34 @@ export function useUiEditorSession(
);
if (saved.status === 'conflict') {
setSaveError('资源已在别处更新;请重新加载后再保存。');
return null;
return {
status: 'failed' as const,
phase: 'save' as const,
message: '资源已在别处更新;请重新加载后再保存。',
};
}
setPersistedRevision(saved.revision);
setSavedStateSignature(snapshotSignature);
return await stateStore.generateCode(resourceId);
try {
return {
status: 'generated' as const,
result: await stateStore.generateCode(resourceId),
};
} catch (cause) {
const message =
cause instanceof Error ? cause.message : String(cause);
setGenerateError(message);
return {
status: 'failed' as const,
phase: 'generate' as const,
message,
};
}
});
} catch (cause) {
setGenerateError(cause instanceof Error ? cause.message : String(cause));
return null;
const message = '保存失败,请稍后重试。';
setSaveError('保存失败,请稍后重试。');
return { status: 'failed' as const, phase: 'save' as const, message };
} finally {
setIsGenerating(false);
setIsSaving(false);