修复 UI 工作流保存生成竞态
统一保存、生成、加载与 AI 工作流动作的忙碌禁用状态 让保存和生成序列共享 editor 状态锁并在动作入口校验 阻止工作流步骤切换在异步操作期间插入
This commit is contained in:
@@ -39,7 +39,7 @@ export function WorkflowActionCard({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-orange-600 px-3 py-1.5 text-xs font-semibold text-white disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={workflow.isAiRunning}
|
||||
disabled={workflow.isBusy}
|
||||
onClick={() => void action.action()}
|
||||
>
|
||||
{running ? action.runningLabel : hasRun ? '重新运行' : action.label}
|
||||
@@ -48,7 +48,7 @@ export function WorkflowActionCard({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-(--platform-subpanel-border) px-3 py-1.5 text-xs disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={workflow.isAiRunning}
|
||||
disabled={workflow.isBusy}
|
||||
onClick={workflow.continueToNextStep}
|
||||
>
|
||||
继续
|
||||
|
||||
@@ -93,12 +93,10 @@ export default function UiEditorPage({
|
||||
|
||||
async function saveAndGenerate() {
|
||||
setGenerateSuccess(null);
|
||||
if (await session.save.save()) {
|
||||
const result = await session.save.generateCode();
|
||||
if (result) {
|
||||
setGenerateAfterWarning(false);
|
||||
setGenerateSuccess(`代码已生成:${result.relativePath}`);
|
||||
}
|
||||
const result = await session.save.saveAndGenerateCode();
|
||||
if (result) {
|
||||
setGenerateAfterWarning(false);
|
||||
setGenerateSuccess(`代码已生成:${result.relativePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +207,7 @@ export default function UiEditorPage({
|
||||
<ToolNavigation
|
||||
activeStep={session.workflow.activeStep}
|
||||
furthestStepIndex={session.workflow.furthestStepIndex}
|
||||
disabled={session.workflow.isAiRunning}
|
||||
disabled={session.workflow.isBusy}
|
||||
onChange={session.workflow.requestStepChange}
|
||||
/>
|
||||
<div className="grid min-h-0 min-w-0 flex-1 grid-cols-[minmax(0,24fr)_minmax(0,44fr)_minmax(0,32fr)] overflow-hidden">
|
||||
|
||||
@@ -372,6 +372,8 @@ export function useUiEditorSession(
|
||||
!isSlaveToDescendant(images, id as UIDesignImageId, activeImageId),
|
||||
);
|
||||
const isAiRunning = isSuggesting || isRecognizing || isBinding || isMerging;
|
||||
const isWorkflowBusy =
|
||||
isAiRunning || isSaving || isGenerating || isLoading || editor.isLocked;
|
||||
const stateSignature = JSON.stringify(editor.state);
|
||||
const isDirty =
|
||||
resourceId !== undefined &&
|
||||
@@ -665,7 +667,7 @@ export function useUiEditorSession(
|
||||
}
|
||||
|
||||
function requestStepChange(step: UiEditorStepId) {
|
||||
if (step === activeStep || isAiRunning) return;
|
||||
if (step === activeStep || isWorkflowBusy) return;
|
||||
const leavingIssues = postCheckIssuesForStep(editor.state, activeStep);
|
||||
const enteringIssues = prerequisiteIssuesForStep(editor.state, step);
|
||||
if (leavingIssues.length === 0 && enteringIssues.length === 0) {
|
||||
@@ -936,7 +938,7 @@ export function useUiEditorSession(
|
||||
}
|
||||
|
||||
async function suggestUiDesignSemantics() {
|
||||
if (isSuggesting) return;
|
||||
if (isSuggesting || isWorkflowBusy) return;
|
||||
setSuggestionStatus(null);
|
||||
setIsSuggesting(true);
|
||||
try {
|
||||
@@ -959,7 +961,7 @@ export function useUiEditorSession(
|
||||
}
|
||||
|
||||
async function recognizeUi() {
|
||||
if (isRecognizing) return;
|
||||
if (isRecognizing || isWorkflowBusy) return;
|
||||
setRecognitionStatus(null);
|
||||
setIsRecognizing(true);
|
||||
try {
|
||||
@@ -984,7 +986,7 @@ export function useUiEditorSession(
|
||||
|
||||
async function mergeUi() {
|
||||
// TODO: This experimental operation is intentionally outside the formal workflow.
|
||||
if (isMerging) return;
|
||||
if (isMerging || isWorkflowBusy) return;
|
||||
setMergeStatus(null);
|
||||
setIsMerging(true);
|
||||
try {
|
||||
@@ -1002,7 +1004,7 @@ export function useUiEditorSession(
|
||||
}
|
||||
|
||||
async function bindComponents() {
|
||||
if (isBinding) return;
|
||||
if (isBinding || isWorkflowBusy) return;
|
||||
setBindingStatus(null);
|
||||
setIsBinding(true);
|
||||
try {
|
||||
@@ -1046,6 +1048,7 @@ export function useUiEditorSession(
|
||||
isSaving ||
|
||||
isGenerating ||
|
||||
isLoading ||
|
||||
isAiRunning ||
|
||||
loadError ||
|
||||
persistedRevision === null ||
|
||||
editor.isLocked
|
||||
@@ -1055,23 +1058,24 @@ export function useUiEditorSession(
|
||||
setSaveError(null);
|
||||
setGenerateError(null);
|
||||
setIsSaving(true);
|
||||
const snapshot = structuredClone(editor.state);
|
||||
const snapshotSignature = JSON.stringify(snapshot);
|
||||
try {
|
||||
const result = await stateStore.save(
|
||||
resourceId,
|
||||
persistedRevision,
|
||||
snapshot,
|
||||
);
|
||||
if (result.status === 'conflict') {
|
||||
setSaveError('资源已在别处更新;请重新加载后再保存。');
|
||||
return false;
|
||||
}
|
||||
setPersistedRevision(result.revision);
|
||||
if (JSON.stringify(editor.state) === snapshotSignature) {
|
||||
setSavedStateSignature(snapshotSignature);
|
||||
}
|
||||
return true;
|
||||
return await editor.runWithStateLocked(async (snapshot) => {
|
||||
const snapshotSignature = JSON.stringify(snapshot);
|
||||
const result = await stateStore.save(
|
||||
resourceId,
|
||||
persistedRevision,
|
||||
snapshot,
|
||||
);
|
||||
if (result.status === 'conflict') {
|
||||
setSaveError('资源已在别处更新;请重新加载后再保存。');
|
||||
return false;
|
||||
}
|
||||
setPersistedRevision(result.revision);
|
||||
if (JSON.stringify(editor.state) === snapshotSignature) {
|
||||
setSavedStateSignature(snapshotSignature);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} catch {
|
||||
setSaveError('保存失败,请稍后重试。');
|
||||
return false;
|
||||
@@ -1086,6 +1090,7 @@ export function useUiEditorSession(
|
||||
isGenerating ||
|
||||
isSaving ||
|
||||
isLoading ||
|
||||
isAiRunning ||
|
||||
loadError ||
|
||||
persistedRevision === null ||
|
||||
editor.isLocked
|
||||
@@ -1096,7 +1101,9 @@ export function useUiEditorSession(
|
||||
setGenerateError(null);
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
return await stateStore.generateCode(resourceId);
|
||||
return await editor.runWithStateLocked(() =>
|
||||
stateStore.generateCode(resourceId),
|
||||
);
|
||||
} catch (cause) {
|
||||
setGenerateError(cause instanceof Error ? cause.message : String(cause));
|
||||
return null;
|
||||
@@ -1105,6 +1112,50 @@ export function useUiEditorSession(
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAndGenerateCode() {
|
||||
if (
|
||||
!resourceId ||
|
||||
isSaving ||
|
||||
isGenerating ||
|
||||
isLoading ||
|
||||
isAiRunning ||
|
||||
loadError ||
|
||||
persistedRevision === null ||
|
||||
editor.isLocked
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
setSaveError(null);
|
||||
setGenerateError(null);
|
||||
setIsSaving(true);
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
return await editor.runWithStateLocked(async (snapshot) => {
|
||||
const snapshotSignature = JSON.stringify(snapshot);
|
||||
const saved = await stateStore.save(
|
||||
resourceId,
|
||||
persistedRevision,
|
||||
snapshot,
|
||||
);
|
||||
if (saved.status === 'conflict') {
|
||||
setSaveError('资源已在别处更新;请重新加载后再保存。');
|
||||
return null;
|
||||
}
|
||||
setPersistedRevision(saved.revision);
|
||||
if (JSON.stringify(editor.state) === snapshotSignature) {
|
||||
setSavedStateSignature(snapshotSignature);
|
||||
}
|
||||
return await stateStore.generateCode(resourceId);
|
||||
});
|
||||
} catch (cause) {
|
||||
setGenerateError(cause instanceof Error ? cause.message : String(cause));
|
||||
return null;
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
input: {
|
||||
projectPath,
|
||||
@@ -1231,6 +1282,7 @@ export function useUiEditorSession(
|
||||
furthestStepIndex,
|
||||
nextStep,
|
||||
isAiRunning,
|
||||
isBusy: isWorkflowBusy,
|
||||
pendingStepChange: pendingWorkflowStepChange,
|
||||
isSuggesting,
|
||||
hasSuggested,
|
||||
@@ -1277,6 +1329,7 @@ export function useUiEditorSession(
|
||||
warnings: () => postCheckIssuesForSave(editor.state),
|
||||
save,
|
||||
generateCode,
|
||||
saveAndGenerateCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user