From 60d8b8fbb2f676ab13b6918e42da6cef5d6d328c Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 01:04:06 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8D=A1=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E3=80=81=E7=83=AD=E9=A2=84=E5=8F=96=E5=8F=AA?= =?UTF-8?q?=E5=96=82=E5=BD=93=E5=89=8D=E6=A0=8F=E7=9B=AE=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=88=A0=E6=8E=89=E6=9C=AA=E6=8E=A5=E7=BA=BF=E7=9A=84=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E7=89=88=E6=9C=AC=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 资源卡 `` 补 `decoding="async"`,把真机 37.2 MPx 量级的 PNG 解码移出主线程。刻意不加 `loading="lazy"`:卡片已经由资源画本的 `IntersectionObserver`(`rootMargin: 160px`,root 指向真正包含卡片的容器)精确门禁,只对进入可见区及预取边界的卡片发 IPC;再叠一层浏览器视口懒加载会让「预取边界 + 热预取 12 张」读到的视口外卡片被压住不解码,等于白预取。`decoding="async"` 不改变何时加载,与既有调度正交。 - 卡片预览的热预取改为只喂当前分页栏目:原先传的是 61 项全量投影,前 12 张可能落在别的栏目,用户看不到的卡片先占满全局 3 个物理读取槽,看得见的卡片反而要排队。 - 为此把 `projectResourcesByCategory`、`resourcePageCategoryKey`、`resourcePageCategories`、`activePageCategory`、`activePageResources` 整块上移到 `useProjectResourceCardPreviews` 之前并删除原位置定义;`projectResourcesByCategory` 只依赖 `canvasResources` 与 `categoryOrder`,其余只在既有依赖之上派生,上移不引入前向引用。`activePageResources` 合并为单一声明(原先在 `activePageLayoutReady` 之前另有一份同定义局部变量)。 - 已知副作用并接受:Hook 的 `identityByResourceId` 只覆盖当前栏目,因此「A 栏目 → B 栏目 → 回到 A」会重新读取 A 的可见卡。这正是「栏目级热预取」的定义,也符合 PRD §3.3.2「项目 / mode 切换必须清理预览缓存」的口径;代价是来回切换栏目各重读一次可见卡,单张实测 1–7 ms 且仍受全局 3 槽限制。 - 删除 `resourcePreviewVersionByResourceId` 的三处引用与 `useState` 声明。它从未被写入过:只被 `new Map()` 初始化和清空,`previewVersionByResourceId` 恒为空串,属死代码。 - 选择「删」而不是「填真值」:填真 manifest revision 会让每次 manifest 变更把全部缓存 identity 失效并触发全量重读;而现有 identity(`projectPath` / `projectId` / `id` / `category` / `path` / `mediaType`)对不可变素材已经足够——路径变了就是另一个资源。本轮不新增失效判据。 - 遗留:`appSurface > keeps the resource preview version cache wired and cleared on project switch` 断言的是刚被删除的 prop 接线,需要同步删掉该断言;该测试文件当时正被另一个 Agent 在途编辑,按分工不在本次改动内,已上报由主人处理。 --- .../src/view/project-development/index.tsx | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index f2a81d7d8..f86ba9696 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -666,6 +666,12 @@ const ResourceCard = memo(function ResourceCard({ onPreviewDecodeError( previewIdentity, @@ -1299,10 +1305,6 @@ export default function ProjectDevelopmentView({ const [hiddenCommittedResourceId, setHiddenCommittedResourceId] = useState< string | null >(null); - const [ - resourcePreviewVersionByResourceId, - setResourcePreviewVersionByResourceId, - ] = useState>(() => new Map()); const [approvalMode, setApprovalMode] = useState('strict'); const [approvalDialogOpen, setApprovalDialogOpen] = useState(false); const [approvalNotice, setApprovalNotice] = useState(''); @@ -1819,16 +1821,50 @@ export default function ProjectDevelopmentView({ ), }; }, [resourceGraph.referenceEdges, resourceGraph.taskFlows, resources]); + // 栏目级资源分组必须早于卡片预览 Hook:当前分页栏目、以及热预取要喂的资源集合 + // 都由它派生。它只依赖 `canvasResources` 与 `categoryOrder`,上移不引入前向引用。 + const projectResourcesByCategory = useMemo( + () => + new Map( + categoryOrder.map((category) => [ + category, + canvasResources.filter((resource) => resource.category === category), + ]), + ), + [canvasResources], + ); + // 当前分页栏目必须早于卡片预览 Hook 求出:热预取只喂当前栏目。 + // 传全量投影时前 12 张可能落在别的栏目,用户看不到的卡片先占满读取槽, + // 看得见的卡片反而要排队。 + const resourcePageCategoryKey = useMemo( + () => createResourceCanvasPageCategorySignature(canvasResources), + [canvasResources], + ); + const resourcePageCategories = useMemo( + () => (resourcePageCategoryKey ? [...categoryOrder] : []), + [resourcePageCategoryKey], + ); + const activePageCategory = + resourcePageCategories.find( + (category) => category === activeResourceCategory, + ) ?? + resourcePageCategories.find( + (category) => (projectResourcesByCategory.get(category)?.length ?? 0) > 0, + ) ?? + resourcePageCategories[0] ?? + null; + const activePageResources = activePageCategory + ? (projectResourcesByCategory.get(activePageCategory) ?? []) + : []; const resourceCardPreviews = useProjectResourceCardPreviews({ projectPath, projectId: manifest.projectId, mode: sortMode, - resources: canvasResources, + resources: activePageResources, canvasRef: resourceCanvasRef, // 资源卡片渲染在资源画本场景里,可见性判定要用真正包含它们的容器。 intersectionRootRef: resourceBookManagerRef, eagerPreviewLimit: 12, - previewVersionByResourceId: resourcePreviewVersionByResourceId, }); const resourceCardSizeByResourceId = useMemo( () => @@ -1948,16 +1984,6 @@ export default function ProjectDevelopmentView({ ), [visibleResources], ); - const projectResourcesByCategory = useMemo( - () => - new Map( - categoryOrder.map((category) => [ - category, - canvasResources.filter((resource) => resource.category === category), - ]), - ), - [canvasResources], - ); const resourceIdsByCategory = useMemo( () => new Map( @@ -1986,23 +2012,6 @@ export default function ProjectDevelopmentView({ token: transitionToken, }); }, [resourceCategoryScopeKey]); - const resourcePageCategoryKey = useMemo( - () => createResourceCanvasPageCategorySignature(canvasResources), - [canvasResources], - ); - const resourcePageCategories = useMemo( - () => (resourcePageCategoryKey ? [...categoryOrder] : []), - [resourcePageCategoryKey], - ); - const activePageCategory = - resourcePageCategories.find( - (category) => category === activeResourceCategory, - ) ?? - resourcePageCategories.find( - (category) => (projectResourcesByCategory.get(category)?.length ?? 0) > 0, - ) ?? - resourcePageCategories[0] ?? - null; const registerResourceBookThumbnail = useCallback( (category: ResourceCategory, element: HTMLElement | null) => { @@ -2287,9 +2296,6 @@ export default function ProjectDevelopmentView({ activePageExtent.y, ], ); - const activePageResources = activePageCategory - ? (projectResourcesByCategory.get(activePageCategory) ?? []) - : []; const activePageLayoutReady = activePageResources.every((resource) => resourcePositionById.has(resource.id), ); @@ -3236,7 +3242,6 @@ export default function ProjectDevelopmentView({ setPendingResourceEditsError(''); setPendingResourceEditActionErrors(new Map()); setResourceEditServiceIdentityConfirmations(new Map()); - setResourcePreviewVersionByResourceId(new Map()); resourceRecoveryPanelEpochRef.current += 1; setResourceRecoveryPanelOpen(false); pendingResourceEditActionIdsRef.current = new Set();