修复资源画本转场克隆遮挡顺序的第二次跳变

两个场景的 z-index 数值本身不同(总览 1-3、子画布 25),只搬数值无法保证顺序一致;且只存在于来源里的淡出克隆被排在最后会盖住存活卡片,消失时表现为跳变
改为按目标场景的最终绘制顺序(z-index 升序,同值再按 DOM 顺序)给克隆分配单调层级,淡出项统一压到最底层
快照记录 DOM 序号,补充 resourceBookController 用例断言层级单调与标题栏在卡片之上
This commit is contained in:
2026-09-09 16:49:53 +08:00
parent 9a3313fe56
commit a655ee1b1e
2 changed files with 29 additions and 14 deletions
@@ -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<string, Snapshot> {
'.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<string, Snapshot> {
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);
@@ -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();
});