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 9a84c336a..a788cf4c3 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 @@ -165,7 +165,6 @@ import { buildResourceBookScenePlan, resourceBookAllBandLocalPoint, type ResourceBookAllLayout, - resourceBookAllTitlebarRects, type ResourceBookOverviewRect, resourceBookSceneCardKey, resourceBookSceneCardLayoutSignature, @@ -1176,26 +1175,32 @@ function ResourceBookScene({ : undefined; return ( -
- -
+ {/* + 展开态的真实栏目不出标题栏:那一页是**一张平铺画布**,只有 `all` 那一条钉在 + 视口上的标题栏(见 `ResourceBookSceneCategoryPlan.titlebar`)。 + */} + {group.titlebar ? ( +
+ +
+ ) : null} {group.cards.map((card) => { const expanded = card.presentation === 'child'; const cardVisible = @@ -2307,20 +2312,6 @@ export default function ProjectDevelopmentView({ resourcePositionById, visibleResourcesByCategory, ]); - /** - * 展开态的栏目标题栏落点。复用总览态"按矩形钉标题栏"的同一条渲染路径, - * 只是矩形来自分带而不是总览缩略卡的测量。 - */ - const resourceBookAllTitlebarRectMap = useMemo( - () => resourceBookAllTitlebarRects(resourceBookAllLayout), - [resourceBookAllLayout], - ); - /** - * 画本场景当前该用哪份标题栏矩形:展开态用分带,其余状态用总览缩略卡的测量结果。 - */ - const resourceBookSceneRects = resourceBookOpensAllResources - ? resourceBookAllTitlebarRectMap - : resourceBookOverviewRects; /** * 展开态的视口拟合(入口与「适应画布」共用一处,避免两份几何口径)。 */ @@ -2990,7 +2981,7 @@ export default function ProjectDevelopmentView({ state: resourceBookState, visibleCategoryOrder: resourceBookSceneCategoryOrder, resourcesByCategory: resourceBookSceneResourcesByCategory, - overviewRects: resourceBookSceneRects, + overviewRects: resourceBookOverviewRects, positions: resourcePositionById, cardSizes: resourceCardSizeByResourceId, visibleResourceIds, @@ -3010,8 +3001,8 @@ export default function ProjectDevelopmentView({ resourceBookAllLayout, resourceBookCategoryViewCenters, resourceBookExitingCards, + resourceBookOverviewRects, resourceBookSceneCategoryOrder, - resourceBookSceneRects, resourceBookSceneResourcesByCategory, resourceBookState, resourceCardSizeByResourceId, @@ -3022,8 +3013,8 @@ export default function ProjectDevelopmentView({ ], ); /** - * 展开态的标题栏几何由分带同步算出,不需要等总览缩略卡测量完(那套是总览态的 `is-measuring` - * 首帧门)。其它状态维持原判据不变。 + * 展开态不需要等总览缩略卡测量完就能画(它的几何来自分带,不来自测量);其它状态维持 + * 原判据不变(总览态的 `is-measuring` 首帧门)。 */ const resourceBookOverviewReady = useMemo( () => @@ -3037,7 +3028,7 @@ export default function ProjectDevelopmentView({ const resourceBookMotionSignatures = useMemo(() => { const signatures = new Map(); for (const group of resourceBookScenePlan) { - const rect = resourceBookSceneRects.get(group.category); + const rect = resourceBookOverviewRects.get(group.category); signatures.set( resourceBookSceneTitleKey(group.category), group.titlebarActive @@ -3049,7 +3040,7 @@ export default function ProjectDevelopmentView({ } } return signatures; - }, [resourceBookScenePlan, resourceBookSceneRects]); + }, [resourceBookScenePlan, resourceBookOverviewRects]); // 拖拽中的卡片只跟指针走,不参与布局 FLIP。 const resourceBookFrozenMotionKeys = useMemo(() => { const keys = new Set(); @@ -6248,7 +6239,7 @@ export default function ProjectDevelopmentView({ { - return new Map( - layout.bands.map((band) => [ - band.category as ResourceBookTarget, - { - left: band.originX, - top: band.titlebarTop, - width: band.width, - height: RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT, - }, - ]), - ); -} - /** * 展开态的世界坐标 → 栏目内局部坐标(**落盘前必须走的唯一换算**)。 * @@ -364,6 +335,14 @@ export type ResourceBookSceneCardPlan = { export type ResourceBookSceneCategoryPlan = { category: ResourceBookTarget; resourceCount: number; + /** + * 这一组要不要渲染栏目标题栏节点。 + * + * 展开态只有 `all` 那一条(钉在视口上、带「资源总览」收起入口):展开态是**一张平铺 + * 画布**,真实栏目只铺卡片。总览态与栏目态每一组都要(总览态靠它承载计数徽标, + * 栏目态靠它承载当前栏目与返回入口)。 + */ + titlebar: boolean; titlebarActive: boolean; titlebarOpacity: number; cards: ResourceBookSceneCardPlan[]; @@ -377,7 +356,8 @@ export type ResourceBookSceneCategoryPlan = { * - 返回总览时源栏目未回到摞上的卡片按子画布布局淡出; * - 搜索/排序导致的卸载由 `exitingCards` 保留一帧,用同一套淡出撤下。 * - 「所有资源」组在总览态只出栏目标题栏(计数即全量资源数),卡片本体仍由各栏目摞承载; - * 在**展开态**则反过来:各真实栏目铺满自己分带里的全部卡片,`all` 只留标题栏与计数。 + * 在**展开态**则反过来:各真实栏目把自己分带里的全部卡片平铺在画布上(**不出标题栏**), + * `all` 只留钉住的标题栏与计数。 */ export function buildResourceBookScenePlan({ state, @@ -453,7 +433,7 @@ export function buildResourceBookScenePlan({ const categoryResources = resourcesByCategory.get(category) ?? []; /** * 展开态:`all` 只出标题栏与全量计数(`titlebarActive` 让它钉在视口上,并带上 - * 「资源总览」收起入口);真实栏目把全部卡片铺在自己那条分带里。 + * 「资源总览」收起入口);真实栏目把全部卡片平铺在自己那条分带里,**不渲染标题栏**。 * * 真实栏目这一支仍然按 `presentation === 'child'` 取卡,而不是"展开态就一定出卡": * "能出卡的判定"只有 `resourceBookCategoryCardPresentation` 一处,避免两边口径分叉。 @@ -463,6 +443,7 @@ export function buildResourceBookScenePlan({ return { category, resourceCount: categoryResources.length, + titlebar: true, titlebarActive: true, titlebarOpacity: 1, cards, @@ -485,8 +466,9 @@ export function buildResourceBookScenePlan({ return { category, resourceCount: categoryResources.length, + titlebar: false, titlebarActive: false, - titlebarOpacity: 1, + titlebarOpacity: 0, cards, }; } @@ -575,6 +557,7 @@ export function buildResourceBookScenePlan({ return { category, resourceCount: categoryResources.length, + titlebar: true, titlebarActive: presentation === 'child', titlebarOpacity: childLayout || state.view === 'main' ? 1 : 0, cards, diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 9a30e2d27..6be8210f6 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -9609,9 +9609,9 @@ export function registerProjectAgentStatusTests() { expect(previewHostRule).toMatch(/position:\s*absolute/); expect(previewHostRule).toMatch(/transform-origin:\s*0 0/); - // 3) 点它进入的是**同一套画本场景**,不是另一条渲染分支:每个真实栏目铺在自己的分带上, - // 卡片宿主是栏目页那一个 `.game-resource-book-scene-card`、`data-resource-book-category` - // 仍是真实栏目;`all` 自己只留钉住的标题栏与全量计数。 + // 3) 点它进入的是**同一套画本场景**,不是另一条渲染分支:真实栏目的卡片平铺在同一张 + // 画布上,卡片宿主是栏目页那一个 `.game-resource-book-scene-card`、 + // `data-resource-book-category` 仍是真实栏目;`all` 自己只留钉住的标题栏与全量计数。 fireEvent.click(screen.getByRole('button', { name: '打开所有资源' })); await waitFor(() => expect( @@ -9651,6 +9651,14 @@ export function registerProjectAgentStatusTests() { ); expect(hostCategories.length).toBeGreaterThan(1); expect(hostCategories).not.toContain('all'); + // 展开态是一张**平铺画布**:真实栏目不出标题栏,整个场景只留 `all` 那一条钉住的。 + // 曾经"每条分带配一条标题栏"时,没有可见资源的栏目拿不到带矩形,标题栏会全部回落到 + // 世界原点叠成一摞(用户报的破版就是这一摞);下面这条把它钉死。 + expect( + document.querySelectorAll( + '.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])', + ), + ).toHaveLength(0); // 每张资源恰好一个宿主(展开态没有"第二份宿主"):条数与全量计数同源, // 因此不会出现「标题说 N 项、画布少几张」。 expect( @@ -9785,6 +9793,13 @@ export function registerProjectAgentStatusTests() { expect( document.querySelectorAll('.game-resource-book-scene-card'), ).toHaveLength(0); + // 空项目恰恰是那一摞的现场:没有任何带 ⇒ 曾经每个真实栏目的标题栏都回落到世界原点 + // 叠在一起(最后画的「项目版本」浮在画布中央)。展开态只允许 `all` 那一条标题栏。 + expect( + document.querySelectorAll( + '.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])', + ), + ).toHaveLength(0); expect( document.querySelector('.game-resource-page-empty')?.textContent, ).toBe('没有匹配资源'); diff --git a/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts b/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts index 20a0dbb0d..e7494b287 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts @@ -4,12 +4,11 @@ import { buildResourceBookAllLayout, buildResourceBookScenePlan, groupResourceBookStacks, - RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT, + RESOURCE_BOOK_ALL_BAND_GAP, RESOURCE_BOOK_OVERVIEW_STACK_LIMIT, type ResourceBookAllBand, resourceBookAllBandLocalPoint, type ResourceBookAllLayout, - resourceBookAllTitlebarRects, resourceBookOverviewCardLayout, type ResourceBookOverviewRect, resourceBookSceneCardKey, @@ -421,7 +420,7 @@ describe('buildResourceBookScenePlan', () => { ['unclassified', resources], ['scene', [sceneResource]], ]), - overviewRects: resourceBookAllTitlebarRects(allLayout), + overviewRects: new Map(), positions: allPositions, cardSizes: allCardSizes, visibleResourceIds: new Set(allResources.map((item) => item.id)), @@ -439,6 +438,7 @@ describe('buildResourceBookScenePlan', () => { )!; // `all` 一张卡都不挂:卡片由各真实栏目承载,否则同一张卡会挂两份、FLIP 键互相顶掉。 expect(allGroup.cards).toEqual([]); + expect(allGroup.titlebar).toBe(true); expect(allGroup.titlebarActive).toBe(true); expect(allGroup.titlebarOpacity).toBe(1); expect(allGroup.resourceCount).toBe(allResources.length); @@ -449,9 +449,11 @@ describe('buildResourceBookScenePlan', () => { expect(artGroup.cards.every((card) => card.presentation === 'child')).toBe( true, ); - // 展开态的栏目标题栏不钉在视口上,但要可见(它随分带一起被视口变换)。 + // 展开态的真实栏目**不出标题栏**:那一页是一张平铺画布,标题栏只有 `all` 那一条。 + // 曾经"每条带配一条标题栏"时,没有可见资源的栏目拿不到带矩形,标题栏会集体回落到 + // 世界原点叠成一摞(用户报的正是这个)。 + expect(artGroup.titlebar).toBe(false); expect(artGroup.titlebarActive).toBe(false); - expect(artGroup.titlebarOpacity).toBe(1); // 画出来的是"栏目内局部坐标 + 带原点",拖动起点与它同系(提交时再减回带原点)。 const first = artGroup.cards[0]!; expect(first.layout.x).toBe(positions.get('art-0')!.x + band.originX); @@ -491,7 +493,7 @@ describe('buildResourceBookScenePlan', () => { [RESOURCE_BOOK_ALL_TARGET, resources], ['unclassified', resources], ]), - overviewRects: resourceBookAllTitlebarRects(allLayout), + overviewRects: new Map(), state: { view: 'main', category: null, @@ -550,13 +552,17 @@ describe('buildResourceBookAllLayout', () => { it('stacks the bands downward so no two categories share one origin', () => { const [first, second] = layout.bands; - // 标题栏横带压在卡片区之上,两个带的首尾不重叠:否则拖上面一带的卡会压到下面一带的标题。 - expect(first!.titlebarTop).toBeLessThan(first!.originY); - expect(second!.titlebarTop).toBeGreaterThan(first!.originY + first!.height); + // 带里只有卡片,所以带宽从原点直接长出 extent 高;两带之间隔一条固定的留白: + // 否则拖上面一带的卡会压到下面一带的卡上。 + expect(first!.originY).toBe(0); + expect(first!.height).toBeGreaterThan(0); + expect(second!.originY).toBe( + first!.originY + first!.height + RESOURCE_BOOK_ALL_BAND_GAP, + ); expect(second!.originY).toBeGreaterThan(first!.originY); }); - it('covers every titlebar and card box in its bounds', () => { + it('covers every card box in its bounds', () => { const last = layout.bands.at(-1)!; expect(layout.bounds.x).toBeLessThanOrEqual(0); expect(layout.bounds.y).toBeLessThanOrEqual(0); @@ -565,25 +571,11 @@ describe('buildResourceBookAllLayout', () => { last.originY + last.height, ); }); - - it('hands every band a titlebar rect so the scene reuses the overview titlebar path', () => { - const rects = resourceBookAllTitlebarRects(layout); - const audio = layout.bandByCategory.get('audio')!; - - expect(rects.get('audio')).toEqual({ - left: audio.originX, - top: audio.titlebarTop, - width: audio.width, - height: RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT, - }); - expect(rects.has('character')).toBe(false); - }); }); describe('resourceBookAllBandLocalPoint', () => { const band: ResourceBookAllBand = { category: 'document', - titlebarTop: 0, originX: 40, originY: 100, width: 620, diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 84ed19fb7..e0d6051f5 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -8469,3 +8469,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 与"历史手动坐标只读恢复、不删除、不重置、不迁移"的关系:**不冲突**。写侧只做"把这一张卡在它自己栏目里的坐标设为手动 + 新 x/y",正是栏目页拖动的同一操作(`moveResourceCanvasPosition` 只改该 `(resourceId, section)` 一条并置 `manuallyPlaced: true`);读时归并(`normalizeResourceCanvasPosition`)与"不触发 automatic 重排"的既有口径都未改动。 - 验证方式:`resourceBookAllBandLocalPoint` 两条单测(减带原点 / 栏目页与无带时原样返回);变异验证:去掉减带原点 → 用例红(实测 `{x:100,y:260}` vs 期望 `{x:100,y:160}`)。真机判据(用户可自证"同一份数据"):在展开态把某张卡拖到新位置 → 进它所属栏目页,同一张卡就在新位置;反向在栏目页拖一张 → 回展开态看它在带内同步。 - 关联文档:`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S7 判据含这条往返)、`apps/ai-game-creator-shell/src/view/project-development/{useProjectResourceCanvasLayout.ts,resourceBookLayout.ts,index.tsx}`。 + +## 2026-09-12 「所有资源」展开态去掉每栏一条标题栏:那一页是一张平铺画布,只留 `all` 那一条 + +- 背景:用户报「所有资源里的 UI 样式有点问题,还有,我希望这里面所有资源都平铺开,和其他普通画布一样」。真机截图(空项目、0 项)里画布中央浮着一块带阴影的「项目版本 0 项」面板,其余栏目看不见——这就是上一版"每条分带配一条标题栏"的直接后果:**分带只对"有可见资源"的栏目生成**(`buildResourceBookAllLayout` 里 `resources.length === 0 → continue`),而标题栏矩形只从分带里取(旧 `resourceBookAllTitlebarRects`),于是空栏目的标题栏拿不到矩形,全部回落到 `ResourceBookScene` 的 `rect?.left ?? 0 / rect?.top ?? 0 / width ?? 280px` 默认值 ⇒ **7 个真实栏目的标题栏在世界原点叠成一摞**,最后画的那个(栏目顺序末位)浮在最上面。空项目恰恰是必现现场:一条带都没有。 +- 决策:**展开态的真实栏目不再渲染栏目标题栏**(`ResourceBookSceneCategoryPlan` 新增 `titlebar: boolean`,展开态真实栏目为 `false`),整个场景只留 `all` 那一条钉在视口上、带「资源总览」收起入口的标题栏。于是「所有资源」在结构上与栏目页完全同形:**一条钉住的标题栏 + 一张平铺的画布**(点阵底、卡片可拖、可滚轮缩放平移、可框选),不再有"每组一个浮动面板"。分带保留(它仍是"局部坐标 → 世界坐标"的唯一偏移,见 2026-09-11 那条拖动写回决策),但带内**只剩卡片**:删掉标题栏横带的高度预留(`RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT`)与它的矩形换算(`resourceBookAllTitlebarRects`),带原点直接等于游标,带间距就是 `RESOURCE_BOOK_ALL_BAND_GAP`。 +- 不做什么:不动总览态与栏目态(两者每一组都要标题栏:总览态靠它承载计数徽标,栏目态靠它承载当前栏目与返回入口);不动拖动写回口径(`resourceBookAllBandLocalPoint` 一行未改);不动转场层;**零 CSS 变更**(`.game-resource-book-scene-titlebar` 规则全部保留,只是展开态少渲染几个节点)。 +- 验证方式:① 单测 `resourceBookLayout.test` 钉住"展开态真实栏目 `titlebar === false`、`all` 为 `true`",并删掉只服务旧标题栏矩形的断言与那个已不存在的导出;② appSurface 两条「所有资源」用例各加一条反向守卫 `document.querySelectorAll('.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])')` 长度为 0(空项目那条正是用户现场);③ **变异验证(已实测红灯)**:把展开态真实栏目的 `titlebar` 改回 `true` → 两条守卫同时红,报 `expected … to have a length of +0 but got 7`——这个 `7` 就是七个真实栏目叠在原点的实证。 +- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/{resourceBookLayout.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/{resourceBookLayout.test.ts,appSurface/project-development.suite.ts}`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S7 行措辞)。 +- 关联:本条**收窄**(不推翻)2026-09-11 的「所有资源」页画本场景决策:那一条定的是"同一套场景 + 同一颗 renderCard + 分带偏移",本条只改"展开态出不出栏目标题栏"。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 740de9efb..e1f01182e 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5354,3 +5354,12 @@ - **处理**:push 前按链路逐条跑,不要只跑子集;`&&` 链在某一步失败时,**必须把失败步之后的每一步单独再跑一遍**(上列 10 步 + `git diff --check`),逐条记 exit code(取 exit code 不要接管道)。凡改动碰到 `package.json` / `.husky/` / `scripts/` 下的门禁资产,**`check:git-hooks` 是必跑项**——它钉住 hook 与 lint-staged 配置的形状,增删键都会让它以 `deepStrictEqual` 失败。 - **验证**:本次 `check:git-hooks` 因本机 Windows EBUSY 中断后,把其后各步单独跑齐:`check:spacetime-schema` / `check:production-ops` / `check:preview-deployer` / `check:maintenance-page` / `lint:eslint` / `typecheck` / `check:encoding` 全部 `exit 0` —— 即「因为前一步红而没跑到」的部分本来是全绿的;只有 `check:rustfmt` 红,且红在别人在途的 `.rs`(`assets.rs:519` 落在其未提交 hunk `+470,62`、`commands.rs:3079` 落在其未提交 hunk `+3068,58`)。变异验证:把 `*.rs` 从 `package.json` 摘掉 → `check:git-hooks` 第 1 个用例以同样的 `deepStrictEqual` operator 变红(`# fail 2`);还原(`package.json` SHA256 与改前一致)后该用例回 `ok`。 - **关联**:`package.json` 的 `lint` / `check:git-hooks` / `check:rustfmt`、`scripts/check-repository-ci.sh`、`scripts/git-hooks.test.mjs`、`.husky/pre-commit`、提交 `dd7cf401a` / `2a7bfadd7` / `15660a98b`。 + +## 2026-09-12 「矩形从哪来」缺一路输入时,别让它静默回落到世界原点:7 条栏目标题栏在空项目里叠成一摞 + +- **现象**:空项目(0 项)进「所有资源」,画布正中央浮着一块带阴影的「项目版本 0 项」面板,其余六个栏目看不见——用户报"UI 样式有点问题"。它们并不是被谁隐藏,而是**七块面板叠在同一个位置**,只看得见最后渲染的那一块。 +- **原因**:`ResourceBookScene` 的栏目标题栏位置只有一路输入 `overviewRects.get(category)`,取不到就用 `rect?.left ?? 0 / rect?.top ?? 0 / width ?? 280px` 兜底。而展开态这一路输入来自分带矩形(旧 `resourceBookAllTitlebarRects`),分带又只对**有可见资源**的栏目生成(`buildResourceBookAllLayout` 里 `resources.length === 0 → continue`)⇒ 空栏目永远拿不到矩形,全部回落到世界原点;栏目顺序七项,最后画的「项目版本」压在最上面。空项目是必现现场(一条带都没有),用户截图正是这种。 +- **识别特征**:出现的不是"某一个错位面板",而是**栏目顺序里最后一项**的名字(本例 = 栏目顺序末位「项目版本」),且它随视口平移一起动 —— 即"坐标系原点上的同类节点堆叠",而不是定位算错。 +- **处理**:让"该不该有这个节点"显式化,而不是靠"矩形取不到就兜底":`ResourceBookSceneCategoryPlan` 增 `titlebar: boolean`,展开态的真实栏目为 `false`(那一页只留 `all` 一条钉住的标题栏),渲染层按它决定是否产出节点;同时删掉只服务旧形态的 `RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT` / `resourceBookAllTitlebarRects`。**`?? 0` 兜底本身不是错,错在它同时承担"没有布局信息"和"布局信息恰好是原点"两种语义**——凡是一个坐标有多路来源(测量 / 计算 / 静态),缺某一路时要么显式不渲染,要么让缺路成为可观测的信号,不要静默给默认值。 +- **验证**:appSurface 两条「所有资源」用例加反向守卫 `querySelectorAll('.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])')` 长度为 0(含空项目那条);变异验证:把展开态真实栏目的 `titlebar` 改回 `true` → 两条同时红,报 `expected … to have a length of +0 but got 7`(7 = 七个真实栏目,实证是"一摞"而不是"一个错位框")。 +- **关联**:`apps/ai-game-creator-shell/src/view/project-development/{resourceBookLayout.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/{resourceBookLayout.test.ts,appSurface/project-development.suite.ts}`、`decision-log.md` 2026-09-12 条。 diff --git a/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md b/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md index 33c295dc3..3eb4f2e27 100644 --- a/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md +++ b/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md @@ -46,7 +46,7 @@ | 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 | | --- | --- | --- | --- | --- | --- | -| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**铺在同一张画布上;全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539–541 | `[data-resource-book-view="main"\|"child"]`、当前栏目 `.game-resource-book-scene-titlebar.is-active[data-resource-book-category]`(展开态这一条是 `data-resource-book-category="all"`,带「资源总览」收起入口)、卡片宿主 `.game-resource-book-scene-card[data-resource-book-category="<真实栏目>"]`(每张资源**恰好一个**宿主)、`[data-resource-book-section-scroll]`。**旧的独立网格判据已随分支删除**:`[data-resource-book-all-page="true"]` / `[data-resource-book-all-host="true"]` / `.game-resource-all-*` 都不应再出现。拖动的判据:在展开态拖一张卡,再进它所属栏目页看同一张卡**落在新位置**(两页共用同一份 `resource-layouts/{dependency,type}.json` 的栏目内局部坐标) | 「所有资源」卡渲染真实预览(`game-resource-book-preview-card`)**已提交**(`ea9668e4a`);旧构建只有占位摞。展开态的视口是内存态(不落盘),只有卡片坐标落盘 | +| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**平铺在同一张画布上(展开态**不出每栏一条标题栏**,整个场景只有 `all` 那一条钉在视口上的标题栏,与栏目页"一条钉住的标题栏 + 一张平铺画布"同形);全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539–541 | `[data-resource-book-view="main"\|"child"]`、当前栏目 `.game-resource-book-scene-titlebar.is-active[data-resource-book-category]`(展开态这一条是 `data-resource-book-category="all"`,带「资源总览」收起入口;**展开态 `data-resource-book-category` 非 `all` 的标题栏一个都不该有**——空栏目曾因拿不到分带矩形而集体回落到世界原点叠成一摞)、卡片宿主 `.game-resource-book-scene-card[data-resource-book-category="<真实栏目>"]`(每张资源**恰好一个**宿主)、`[data-resource-book-section-scroll]`。**旧的独立网格判据已随分支删除**:`[data-resource-book-all-page="true"]` / `[data-resource-book-all-host="true"]` / `.game-resource-all-*` 都不应再出现。拖动的判据:在展开态拖一张卡,再进它所属栏目页看同一张卡**落在新位置**(两页共用同一份 `resource-layouts/{dependency,type}.json` 的栏目内局部坐标) | 「所有资源」卡渲染真实预览(`game-resource-book-preview-card`)**已提交**(`ea9668e4a`);旧构建只有占位摞。展开态的视口是内存态(不落盘),只有卡片坐标落盘 | | **S8** 搜索 | 点右下角 `搜索资源` 按钮或按 `Ctrl/Cmd+F` → 输入 → Esc | 浮层收起**不清搜索条件**;`type=search` 原生 Esc 清空被 `preventDefault` 挡掉;存在搜索时 Dock 按钮高亮 | L55、L322(搜索只隐藏卡片、不重排坐标) | `button[aria-label="搜索资源"]` 的 `aria-expanded`、`title="搜索资源(Ctrl/Cmd+F)"`、`.is-active`;输入框 `input[type=search][aria-label="搜索项目资源"]`,其 `id` 等于按钮的 `aria-controls` | 匹配字段 = **名称 / 路径 / 媒体类型 / 任务标题**,不含 `sourceLabel` | | **S9** 选择与多选 | 单击卡片 → `Shift`/`Ctrl`/`Meta` 再点 → 空白处拖拽框选 | 点卡 = 选中并在卡片旁浮出工具条;三种修饰键等效追加;框选可见选框 | L51、L61–62、L318–319 | `data-resource-card-id` + class 含 `is-selected` + `aria-pressed="true"`;浮出容器 `role="toolbar"`(图片 `aria-label="图片工具栏"`,音频 `"素材工具栏"`) | ⚠️ **PRD L61–62 要求的「打开详情」按钮在代码中不存在**(测试显式断言为 `null`),按 Issue #309 C3「取消资源详情面板与全屏编辑路由」判,不要判为缺失;真正入口是 `aria-label="选中资源:<栏目> <名>"` 按钮。**只读信息浮层已回归**(2026-09-11,工具条「信息」动作,见 `decision-log`):**C3 取消的仍只是可编辑详情面板与全屏编辑路由** | | **S10** 快速编辑(图片) | 选中 PNG/JPEG/WEBP 卡 → 工具条「快速编辑」→ 输入 → 提交(提示词下方另有「AI 润色」) | 先 `normalize_local_project_raster_resource`,再 `derive_local_project_resource(editKind='image-reference')` → 服务端 `/api/editor/images/edits`;**产出一张新素材**,源素材与源文件不变,新资源带 `referenceResourceIds` 血缘 | L123、L193、L476–478、L557;#309 C3/C10 | 提交后 `assets[]` +1、卡片 +1 且 `data-resource-card-id` 为新 id;**源卡仍在且内容未变**;工具条真渲染的只有 `quick-edit` 与 `download`;「AI 润色」`aria-label="AI 润色"` + `aria-busy`,成功回填并出现「恢复原文」,失败保留原文并给「AI 润色失败,可重试」 | ① 服务端白名单 17 项(`editor_project.rs:4120-4141`),`character-animation`/`video`/`audio`/`document`/`code` 等**仍会 400**;② 工具条 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画)按 opt-in **不渲染**;③ 润色结果超 `resourceEditPromptMaxLength` 会**截断并提示「已按长度上限截断」**;④ 提示词变了(改写或润色回填)必须换 `operationId`/幂等键,否则 Rust `request_fingerprint` 判「已绑定到不同资源编辑请求」 |