From 985db358a7769c9ed50cc861af48565b5491e3e5 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 9 Sep 2026 16:22:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90=E7=94=BB?= =?UTF-8?q?=E6=9C=AC=E8=BD=AC=E5=9C=BA=E5=85=8B=E9=9A=86=E7=9A=84=E5=B1=82?= =?UTF-8?q?=E7=BA=A7=E4=B8=8E=E9=9D=99=E6=80=81=E4=B8=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 转场层里的克隆只按自身 DOM 顺序叠放,而静态场景中卡片是 25、标题栏是 30,导致动画期间卡片盖住标题栏、结束后又瞬间跳回下方 快照记录元素自身或最近定位祖先的 z-index,克隆按该值设置层级,保持静态层叠关系 补充 resourceBookController 层级回归用例 --- .../resourceBookController.ts | 21 ++++++++++ .../tests/resourceBookController.test.ts | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts index 9998161e2..edfedcc9c 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts @@ -5,6 +5,7 @@ type Snapshot = { key: string; rect: { left: number; top: number; width: number; height: number }; opacity: number; + zIndex: number; content: HTMLElement; }; @@ -43,6 +44,24 @@ function freezeBlobImageSource(image: HTMLImageElement) { } } +/** + * 转场层里的克隆只按自己的 DOM 顺序叠放,和静态场景的层级不一致:卡片会盖住 + * 标题栏,动画结束再瞬间跳回下方。取元素自身或最近定位祖先的 z-index,让克隆 + * 保持静态时的层叠关系。 + */ +function effectiveZIndex(element: HTMLElement, root: HTMLElement) { + for ( + let node: HTMLElement | null = element; + node; + node = node.parentElement + ) { + const parsed = Number.parseInt(getComputedStyle(node).zIndex, 10); + if (Number.isFinite(parsed)) return parsed; + if (node === root) break; + } + return 0; +} + // Freeze presentation only. Clones cannot expose duplicate controls, replay // media, or depend on CSS selectors belonging to the old ancestor tree. function clonePresentation( @@ -188,6 +207,7 @@ function capture(root: HTMLElement): Map { key, rect, opacity, + zIndex: effectiveZIndex(element, root), content: clonePresentation( element.dataset.motionSnapshot ? (element.firstElementChild as HTMLElement) @@ -285,6 +305,7 @@ export function createResourceBookTransitionController() { height: `${item.rect.height}px`, transformOrigin: '0 0', pointerEvents: 'none', + zIndex: String(item.zIndex), }); wrapper.append(item.content); layer.append(wrapper); diff --git a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts index 1b2521b90..4d238116f 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts @@ -138,6 +138,46 @@ describe('resource book FLIP controller', () => { controller.dispose(); }); + it('keeps the static stacking order inside the transition layer', () => { + Object.defineProperty(Element.prototype, 'animate', { + configurable: true, + writable: true, + value: () => ({ + finished: Promise.resolve({} as Animation), + cancel: () => undefined, + }), + }); + const root = document.createElement('div'); + root.innerHTML = `
+
标题
+
+
`; + document.body.append(root); + vi.spyOn(root, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 800, 600)); + for (const element of root.querySelectorAll( + '.game-resource-book-scene-titlebar, .game-resource-card', + )) { + vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(rect()); + } + + const controller = createResourceBookTransitionController(); + const token = controller.begin(root); + controller.play(root, token, () => undefined); + + const wrappers = Array.from( + root.querySelectorAll('[data-motion-snapshot]'), + ); + const titleWrapper = wrappers.find((element) => + element.dataset.motionSnapshot?.startsWith('title:'), + ); + const cardWrapper = wrappers.find((element) => + element.dataset.motionSnapshot?.startsWith('card:'), + ); + expect(Number(titleWrapper?.style.zIndex)).toBe(30); + expect(Number(cardWrapper?.style.zIndex)).toBe(25); + controller.dispose(); + }); + it('retargets from the current overlay geometry and ignores old completion', async () => { const { root, measure, flights } = setup(); const controller = createResourceBookTransitionController();