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 41a04cc08..13acf00ee 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 @@ -6,6 +6,7 @@ type Snapshot = { rect: { left: number; top: number; width: number; height: number }; opacity: number; zIndex: number; + order: number; content: HTMLElement; }; @@ -169,6 +170,7 @@ function capture(root: HTMLElement): Map { '.game-resource-book-scene .game-resource-card, .game-resource-book-scene-titlebar', ), ); + let order = 0; for (const element of elements) { const box = element.getBoundingClientRect(); const rect = { @@ -208,6 +210,7 @@ function capture(root: HTMLElement): Map { rect, opacity, zIndex: effectiveZIndex(element, root), + order: order++, content: clonePresentation( element.dataset.motionSnapshot ? (element.firstElementChild as HTMLElement) @@ -285,23 +288,29 @@ export function createResourceBookTransitionController() { return; } const target = capture(manager); - // 先按目标场景的 DOM 顺序排,再补上只存在于来源里的 key。层级相同的卡片 - // 由 DOM 顺序决定遮挡关系,顺序不一致会让动画结束的瞬间发生跳变。 - const keys = [ - ...target.keys(), - ...Array.from(source.keys()).filter((key) => !target.has(key)), - ]; if (!target.size) { settle(); return; } + // 两个场景的 z-index 数值本身不同(总览 1–3、子画布 25),只搬数值无法保证 + // 顺序一致。按目标场景的最终绘制顺序(z-index 升序,同值再按 DOM 顺序) + // 给克隆分配单调层级,动画期间的遮挡关系就与结束后的静止状态完全一致; + // 只存在于来源里的淡出项统一压到最底层,不会盖住存活卡片。 + const ordered = [ + ...Array.from(target.values()).sort( + (left, right) => + left.zIndex - right.zIndex || left.order - right.order, + ), + ...Array.from(source.values()).filter( + (entry) => !target.has(entry.key), + ), + ]; try { - for (const key of keys) { - const from = source.get(key); - const to = target.get(key); - const item = to ?? from!; + for (const [stackOrder, item] of ordered.entries()) { + const from = source.get(item.key); + const to = target.get(item.key); const wrapper = document.createElement('div'); - wrapper.dataset.motionSnapshot = key; + wrapper.dataset.motionSnapshot = item.key; Object.assign(wrapper.style, { position: 'absolute', left: `${item.rect.left}px`, @@ -310,7 +319,7 @@ export function createResourceBookTransitionController() { height: `${item.rect.height}px`, transformOrigin: '0 0', pointerEvents: 'none', - zIndex: String(item.zIndex), + zIndex: String(stackOrder + 1), }); 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 e041fbf50..05801ec23 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts @@ -173,8 +173,10 @@ describe('resource book FLIP controller', () => { const cardWrapper = wrappers.find((element) => element.dataset.motionSnapshot?.startsWith('card:'), ); - expect(Number(titleWrapper?.style.zIndex)).toBe(30); - expect(Number(cardWrapper?.style.zIndex)).toBe(25); + // 克隆按目标场景的最终绘制顺序分配单调层级:标题栏必须在卡片之上。 + expect(Number(titleWrapper?.style.zIndex)).toBeGreaterThan( + Number(cardWrapper?.style.zIndex), + ); controller.dispose(); }); @@ -215,6 +217,10 @@ describe('resource book FLIP controller', () => { 'card:b', 'card:a', ]); + // 同层级时靠 DOM 顺序决定遮挡:目标顺序里 b 在前,b 的层级必须更低。 + expect(Number(wrappers[0]?.style.zIndex)).toBeLessThan( + Number(wrappers[1]?.style.zIndex), + ); controller.dispose(); });