修复 UI 编辑器保存与代码生成状态

普通保存和资源切换时清理生成提示与进行中状态。

为生成前置条件拦截补充可见错误,并修正保存确认按钮文案。

补齐 IUiDesignStateStore 测试替身的必需非 nullable generateCode 实现。

修正 Rust HTML 树分隔注释并同步更新 UI 编辑会话约定。
This commit is contained in:
2026-09-01 17:03:20 +08:00
parent 96f971df5b
commit 43ab4464ed
5 changed files with 21 additions and 2 deletions
@@ -18,7 +18,7 @@ pub(crate) fn render_ui_design_state_html(state: &State) -> Result<String, Strin
let mut trees = Vec::with_capacity(state.ui_trees.len());
for (index, tree) in state.ui_trees.iter().enumerate() {
if index > 0 {
trees.push("\n<!-- -------------------- -->\n".to_string());
trees.push("\n<!-- ui-tree-separator -->\n".to_string());
}
trees.push(render_tree(state, tree)?.into_string());
}
@@ -94,6 +94,7 @@ export default function UiEditorPage({
if (!resourceId || session.save.isSaving || session.workflow.isAiRunning) {
return;
}
setGenerateSuccess(null);
setSaveAfterReturn(afterReturn);
setGenerateAfterWarning(false);
if (session.save.hasWarnings()) {
@@ -355,7 +356,7 @@ export default function UiEditorPage({
}
}}
>
仍然保存
{generateAfterWarning ? '仍然保存并生成' : '仍然保存'}
</button>
</div>
</ThemedModal>
@@ -257,6 +257,8 @@ export function useUiEditorSession(
setPersistedRevision(0);
setSavedStateSignature(JSON.stringify(EMPTY_UI_EDITOR_STATE));
setSaveError(null);
setGenerateError(null);
setIsGenerating(false);
setHiddenNodeIds(new Set());
return;
}
@@ -265,6 +267,9 @@ export function useUiEditorSession(
setIsLoading(true);
setLoadError(null);
setPersistedRevision(null);
setSaveError(null);
setGenerateError(null);
setIsGenerating(false);
void stateStore
.load(resourceId)
.then(({ state, revision }) => {
@@ -1048,6 +1053,7 @@ export function useUiEditorSession(
return false;
}
setSaveError(null);
setGenerateError(null);
setIsSaving(true);
const snapshot = structuredClone(editor.state);
const snapshotSignature = JSON.stringify(snapshot);
@@ -1084,6 +1090,7 @@ export function useUiEditorSession(
persistedRevision === null ||
editor.isLocked
) {
setGenerateError('当前状态不允许生成代码,请稍后重试。');
return null;
}
setGenerateError(null);
@@ -137,6 +137,7 @@ async function renderLoadedSession(state: State) {
const stateStore: IUiDesignStateStore = {
load: vi.fn().mockResolvedValue({ revision: 0, state }),
save: vi.fn(),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
const hook = renderHook(() =>
useUiEditorSession('/tmp/ui-editor', 'ui-resource', stateStore),
@@ -150,6 +151,7 @@ describe('UiEditorPage', () => {
const stateStore: IUiDesignStateStore = {
load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)),
save: vi.fn(),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
render(
@@ -193,6 +195,7 @@ describe('UiEditorPage', () => {
state: stateWithPages(['page']),
}),
save: vi.fn(),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
const hook = renderHook(
({ resourceId }) =>
@@ -248,6 +251,7 @@ describe('UiEditorPage', () => {
const stateStore: IUiDesignStateStore = {
load: vi.fn(() => new Promise(() => undefined)),
save: vi.fn(),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
const hook = renderHook(() =>
useUiEditorSession('/tmp/ui-editor', 'ui-resource', stateStore),
@@ -355,6 +359,7 @@ describe('UiEditorPage', () => {
state: stateWithPages(['gameplay-page']),
}),
save: vi.fn(),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
render(
@@ -594,6 +599,7 @@ describe('UiEditorPage', () => {
const stateStore: IUiDesignStateStore = {
load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)),
save: vi.fn().mockRejectedValue(new Error('临时存储不可用')),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
render(
createElement(UiEditorPage, {
@@ -627,6 +633,7 @@ describe('UiEditorPage', () => {
status: 'conflict',
currentRevision: 1,
}),
generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')),
};
render(
createElement(UiEditorPage, {
@@ -16,3 +16,7 @@
预览树在会话边界归一化为 `UITree | null`:尚未选择界面图、正在加载或尚未识别树时均以 `null` 表示,不能把 `Array.find` 的 `undefined` 传播到视图接口。
保存与代码生成共享同一份持久化 State/revision。会话层在保存或生成进行期间互斥拦截,且代码生成必须基于已加载的持久化 revision;视图层的保存按钮和“保存并返回”按钮同步遵守该互斥状态。
`UiDesignStateStore` 的 `generateCode(assetId)` 是必需能力,返回成功结果时不得为 nullable;所有注入的 adapter 与测试替身都必须实现该方法。
资源切换时,会话必须清理上一资源的保存/生成错误和生成中状态;普通保存开始时也清理代码生成错误。生成请求若因加载、锁定或 revision 等前置条件被拦截,必须向视图提供可见错误,而不是静默返回。