From 588d0d4728e8003aa1c040154d1a5c2c28800fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 14 Aug 2026 12:15:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20UI=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=89=8D=E7=BD=AE=E6=9D=A1=E4=BB=B6=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=92=8C=E8=B5=84=E6=BA=90=E9=A2=84=E8=A7=88=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 UI 树遍历逻辑,改用 `root` 替代 `children`。 - 添加资源预览作用域和请求 ID,防止并发冲突。 - 调整资源导入模块以支持作用域级预览管理。 - 优化界面设计建议合并逻辑,新增节点继承规则。 --- .../src/components/AssetImporter/index.tsx | 21 ++++++++- .../src/features/ui-editor/importAdapter.ts | 16 ++++++- .../src/features/ui-editor/prerequisites.ts | 4 +- .../features/ui-editor/uiDesignSuggestions.ts | 46 ++++++++++++------- .../src/services/clientApi.ts | 2 +- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx b/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx index f98b6048e..61632795d 100644 --- a/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx +++ b/apps/ai-game-creator-shell/src/components/AssetImporter/index.tsx @@ -11,6 +11,11 @@ import { readClientAssetBytes, resolveClientAssetReadUrl, } from '../../services/clientApi'; +import { + cancelLocalProjectResourcePreviewScope, + createProjectResourcePreviewRequestId, + createProjectResourcePreviewScopeId, +} from '../../services/projectResourcePreviewTransport'; import { ThemedModal } from '../modal/ThemedModal'; import { buildProjectFiles, @@ -71,6 +76,16 @@ export function AssetImporter({ const [currentPath, setCurrentPath] = useState(ROOT_PATH); const currentPathRef = useRef(ROOT_PATH); const remoteLoadedRef = useRef(false); + const previewScopeIdRef = useRef(createProjectResourcePreviewScopeId()); + + useEffect(() => { + const previousScopeId = previewScopeIdRef.current; + cancelLocalProjectResourcePreviewScope(previousScopeId); + previewScopeIdRef.current = createProjectResourcePreviewScopeId(); + return () => { + cancelLocalProjectResourcePreviewScope(previewScopeIdRef.current); + }; + }, [projectPath]); const loadLocal = useCallback(async () => { setError(null); @@ -213,6 +228,8 @@ export function AssetImporter({ void invoke('read_local_project_image_preview', { projectPath, relativePath: item.asset?.localPath, + scopeId: previewScopeIdRef.current, + requestId: createProjectResourcePreviewRequestId(), }) .then((value) => setPreview(value.dataUrl)) .catch(() => setPreview(null)); @@ -226,7 +243,9 @@ export function AssetImporter({ title: '选择图片素材', multiple: true, directory: false, - filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'webp'] }], + filters: [ + { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'webp'] }, + ], }); const sourcePaths = Array.isArray(paths) ? paths : paths ? [paths] : []; if (!sourcePaths.length) return; diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/importAdapter.ts b/apps/ai-game-creator-shell/src/features/ui-editor/importAdapter.ts index 9b5523e82..a35a00317 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/importAdapter.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/importAdapter.ts @@ -1,6 +1,10 @@ import { invoke } from '@tauri-apps/api/core'; import type { ImportedAsset } from '../../components/AssetImporter'; +import { + createProjectResourcePreviewRequestId, + createProjectResourcePreviewScopeId, +} from '../../services/projectResourcePreviewTransport'; import type { SpriteAsset } from './types/SpriteAsset'; import type { UIDesignImage } from './types/UIDesignImage'; import type { UIDesignImageId } from './types/UIDesignImageId'; @@ -35,10 +39,16 @@ export function decodeImageSize(src: string): Promise<[number, number]> { async function readImportedImage( projectPath: string, asset: ImportedAsset, + scopeId: string, ) { const preview = await invoke( 'read_local_project_image_preview', - { projectPath, relativePath: asset.localPath }, + { + projectPath, + relativePath: asset.localPath, + scopeId, + requestId: createProjectResourcePreviewRequestId(), + }, ); return { previewUrl: preview.dataUrl, @@ -50,11 +60,13 @@ export async function prepareDesignImageBatch( projectPath: string, assets: readonly ImportedAsset[], ): Promise>> { + const scopeId = createProjectResourcePreviewScopeId(); return Promise.all( assets.map(async (asset) => { const { previewUrl, pixelSize } = await readImportedImage( projectPath, asset, + scopeId, ); const image: UIDesignImage = { metadata: { @@ -79,11 +91,13 @@ export async function prepareSpriteAssetBatch( projectPath: string, assets: readonly ImportedAsset[], ): Promise>> { + const scopeId = createProjectResourcePreviewScopeId(); return Promise.all( assets.map(async (asset) => { const { previewUrl, pixelSize } = await readImportedImage( projectPath, asset, + scopeId, ); const resource: SpriteAsset = { asset_id: asset.id, diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts b/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts index 7b7bbcc79..2cbae566d 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts @@ -107,7 +107,7 @@ export function validateLayoutGenerationPrerequisites( message: '请先导入并绑定独立素材', }); } - const visit = (nodes: State['ui_trees'][number]['children']) => { + const visit = (nodes: State['ui_trees'][number]['root']['children']) => { for (const node of nodes) { for (const component of node.components) { if ( @@ -136,7 +136,7 @@ export function validateLayoutGenerationPrerequisites( visit(node.children); } }; - for (const tree of state.ui_trees) visit(tree.children); + for (const tree of state.ui_trees) visit([tree.root]); return issues; } diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignSuggestions.ts b/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignSuggestions.ts index e772f8870..6dda7b1c8 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignSuggestions.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignSuggestions.ts @@ -1,36 +1,50 @@ import type { State } from './types/State'; -import type { UIDesignSuggestion } from './types/UIDesignSuggestion'; +import type { UIDesignImageId } from './types/UIDesignImageId'; +import type { UIDesignSuggestionTreeNode } from './types/UIDesignSuggestionTreeNode'; /** - * Applies semantic suggestions conservatively: null means "do not modify", - * an already curated value wins, and suggestions for resources that disappeared - * from the current State are ignored. + * Applies semantic suggestions conservatively. The tree carries relationships; + * existing metadata remains authoritative whenever it is already populated. + * Persisted `slave_to` remains the owning Page ID, so nested descendants inherit + * their root Page rather than pointing at an intermediate Section. */ export function applyUiDesignSuggestions( state: State, - suggestions: readonly UIDesignSuggestion[], + suggestions: readonly UIDesignSuggestionTreeNode[], ): State { const next = structuredClone(state); - for (const suggestion of suggestions) { - const image = next.ui_design_images[suggestion.ui_design_image_id]; - if (!image) continue; + function applyNode( + node: UIDesignSuggestionTreeNode, + pageId: UIDesignImageId | null, + ): void { + const image = next.ui_design_images[node.id]; + if (!image) return; - if (image.metadata.name.trim().length === 0 && suggestion.name?.trim()) { - image.metadata.name = suggestion.name.trim(); + if (image.metadata.name.trim().length === 0 && node.name.trim()) { + image.metadata.name = node.name.trim(); } - if (image.metadata.role === null && suggestion.role !== null) { - image.metadata.role = suggestion.role; + if (image.metadata.role === null) { + image.metadata.role = node.role; } - if (image.metadata.slave_to === null && suggestion.slave_to !== null) { - image.metadata.slave_to = suggestion.slave_to; + if (image.metadata.slave_to === null && pageId !== null) { + image.metadata.slave_to = pageId; } if ( image.metadata.description.trim().length === 0 && - suggestion.description?.trim() + node.description.trim() ) { - image.metadata.description = suggestion.description.trim(); + image.metadata.description = node.description.trim(); } + + const childPageId = node.role === 'Page' ? node.id : pageId; + for (const child of node.children) { + applyNode(child, childPageId); + } + } + + for (const root of suggestions) { + applyNode(root, null); } return next; diff --git a/apps/ai-game-creator-shell/src/services/clientApi.ts b/apps/ai-game-creator-shell/src/services/clientApi.ts index a82648ae6..6ce2d6903 100644 --- a/apps/ai-game-creator-shell/src/services/clientApi.ts +++ b/apps/ai-game-creator-shell/src/services/clientApi.ts @@ -111,7 +111,7 @@ export async function requestClientApiBytes( } let response: Response; try { - response = await fetch(resolveClientApiUrl(url), { + response = await fetchClientHttp(url, { ...init, credentials: 'same-origin', headers,