接入 UI 编辑器保存结果弹窗
让普通保存和保存并生成流程返回结构化结果 将成功与失败反馈接入独立弹窗并保留保存并返回直返行为
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user