From 9a3313fe56f326bd06804c387ad5fa5f85758e72 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 9 Sep 2026 16:43:54 +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=E9=81=AE?= =?UTF-8?q?=E6=8C=A1=E9=A1=BA=E5=BA=8F=E8=B7=B3=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 层级相同的卡片由 DOM 顺序决定遮挡,而转场层原先按来源 key 在前追加克隆,和动画结束后的目标场景顺序不一致,结束瞬间会跳变 改为先按目标场景 DOM 顺序排列,再补只存在于来源里的 key 补充 resourceBookController 用例覆盖来源与目标顺序不同时的克隆顺序 --- .../resourceBookController.ts | 7 +++- .../tests/resourceBookController.test.ts | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) 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 edfedcc9c..41a04cc08 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 @@ -285,7 +285,12 @@ export function createResourceBookTransitionController() { return; } const target = capture(manager); - const keys = new Set([...source.keys(), ...target.keys()]); + // 先按目标场景的 DOM 顺序排,再补上只存在于来源里的 key。层级相同的卡片 + // 由 DOM 顺序决定遮挡关系,顺序不一致会让动画结束的瞬间发生跳变。 + const keys = [ + ...target.keys(), + ...Array.from(source.keys()).filter((key) => !target.has(key)), + ]; if (!target.size) { settle(); return; diff --git a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts index 4d238116f..e041fbf50 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts @@ -178,6 +178,46 @@ describe('resource book FLIP controller', () => { controller.dispose(); }); + it('appends clones in the target DOM order so equal z-index cards keep their occlusion', () => { + 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-card', + )) { + vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(rect()); + } + + const controller = createResourceBookTransitionController(); + const token = controller.begin(root); + const cardWrapper = root + .querySelector('[data-resource-card-id="a"]')! + .closest('.game-resource-book-scene-card')!; + cardWrapper.parentElement!.append(cardWrapper); + controller.play(root, token, () => undefined); + + const wrappers = Array.from( + root.querySelectorAll('[data-motion-snapshot]'), + ); + expect(wrappers.map((element) => element.dataset.motionSnapshot)).toEqual([ + 'card:b', + 'card:a', + ]); + controller.dispose(); + }); + it('retargets from the current overlay geometry and ignores old completion', async () => { const { root, measure, flights } = setup(); const controller = createResourceBookTransitionController();