fix(lightbox): 图集切换不重算几何导致图片压扁
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled

gotoIndex 换图时只替换 src,target/baseW/baseH/transform 全部沿用
第一张图的几何,宽高比不同的图被套用错误的 scale(显示宽高比 =
自身宽高比 × 首图宽高比)。同时 .lightbox-img 的 object-fit: contain
与 transformFor 依赖的「位图填满布局盒」前提冲突,素材网格的方形
缩略图容器下首图即被压扁。

修复:
- swap() 在新图加载后按 naturalWidth/Height 重算 fitCentered 目标,
  布局盒设为 target 尺寸、transform 归位 scale(1,1),baseW/H 同步
  供关闭飞回动画复用
- 先换 src 再判 complete,避免用旧图的 complete 误判缓存命中
- 移除 .lightbox-img 的 object-fit: contain,恢复位图填满布局盒

新增回归测试:切换后几何必须按新图宽高比重算(红绿验证通过)
This commit is contained in:
xfy 2026-07-24 18:15:11 +08:00
parent 051ca947e3
commit 99d33b54d2
3 changed files with 65 additions and 8 deletions

View File

@ -376,18 +376,39 @@ function gotoIndex(rawIndex: number): void {
// 150ms 后换图淡入
const swap = (): void => {
if (!state) return; // 切换中可能已关闭
// 换图后必须按新图真实尺寸重算几何,否则新图沿用第一张的
// target/scale宽高比不同的图会被压扁/拉伸。
// 布局盒直接设为 target 尺寸(宽高比 = 新图transform 归位 scale(1,1)
// baseW/H 同步为 target 尺寸,关闭/滚动关闭的飞回动画以它为 scale 基准。
const applyGeometry = (): void => {
if (!state) return;
const naturalW = s.img.naturalWidth || 1;
const naturalH = s.img.naturalHeight || 1;
const target = fitCentered(naturalW, naturalH, window.innerWidth, window.innerHeight);
s.target = target;
s.baseW = target.w;
s.baseH = target.h;
// 几何跳变不播动画transition 只作用于随后的 opacity 淡入)。
s.img.style.transition = 'none';
s.img.style.width = `${target.w}px`;
s.img.style.height = `${target.h}px`;
s.img.style.transform = transformFor(target, target.w, target.h);
};
const fade = (): void => {
if (!state) return;
s.img.style.transition = 'opacity 150ms ease-out';
s.img.style.opacity = '1';
};
if (s.img.complete && s.img.naturalWidth) {
// 缓存命中,直接淡入
s.img.src = origSrc;
const onReady = (): void => {
applyGeometry();
fade();
} else {
s.img.addEventListener('load', fade, { once: true });
};
// 先换 src 再判 complete换之前判的是旧图必命中会按旧图尺寸算几何。
s.img.src = origSrc;
if (s.img.complete && s.img.naturalWidth) {
onReady(); // 缓存命中,新图尺寸同步可用
} else {
s.img.addEventListener('load', onReady, { once: true });
}
s.caption.textContent = altText;
s.caption.style.display = altText ? '' : 'none';

View File

@ -63,6 +63,13 @@ function getCounter(): HTMLElement | null {
return document.querySelector('.lightbox-counter');
}
/** stub 灯箱图的 natural 尺寸与 completehappy-dom 不加载真实图片)。 */
function stubNatural(img: HTMLImageElement, w: number, h: number): void {
Object.defineProperty(img, 'naturalWidth', { configurable: true, value: w });
Object.defineProperty(img, 'naturalHeight', { configurable: true, value: h });
Object.defineProperty(img, 'complete', { configurable: true, value: true });
}
/** 模拟元素 click真实事件派发触发 addEventListener('click'))。 */
function clickEl(el: Element): void {
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
@ -235,6 +242,33 @@ describe('lightbox 黑盒行为', () => {
const fullB = imgs[1].querySelector('.blur-img-full');
expect(document.activeElement).toBe(fullB);
});
it('切换后按新图宽高比重算几何(不沿用第一张的 target/scale', () => {
const imgs = [makeGalleryImage('/a.webp', 'A'), makeGalleryImage('/b.webp', 'B')];
mountRoot(imgs);
window.__initLightbox('.post-content');
clickEl(imgs[0]);
const lbImg = getLightboxImg()!;
// happy-dom 不真实加载图片:手动 stub 第 1 张 natural 尺寸并派发 load
// 让 openLightbox 的 start() 走完state.target/baseW 按第 1 张建立)。
stubNatural(lbImg, 1000, 500);
lbImg.dispatchEvent(new Event('load'));
vi.advanceTimersByTime(50); // double-rAF 在 fake timers 下同步推进
// 切到第 2 张前,先把 natural stub 成新图尺寸(竖图),模拟缓存命中
pressKey('ArrowRight');
stubNatural(lbImg, 500, 1000);
vi.advanceTimersByTime(200); // 150ms 淡出后 swap 同步执行
// 几何必须按 500x1000 重算:布局盒 = fitCentered 目标尺寸(宽高比 0.5
// transform 归位 scale(1,1)。修复前这里仍是第 1 张的 width/height/scale。
const w = parseFloat(lbImg.style.width);
const h = parseFloat(lbImg.style.height);
expect(w).toBeGreaterThan(0);
expect(w / h).toBeCloseTo(0.5, 2);
expect(lbImg.style.transform).toContain('scale(1,1)');
});
});
describe('重复初始化幂等SPA 数据刷新场景)', () => {

View File

@ -12,8 +12,11 @@
/* 灯箱图片绝对定位尺寸/位置由 JS transform 控制
transform-origin top left配合 translate+scale 实现原地缩放
显式 width/height JS 设置为原图尺寸确保布局尺寸确定
transform 只做视觉缩放不改变占位 */
显式 width/height JS 设置打开时 = 缩略图 rect图集切换后 = 居中目标
尺寸确保布局尺寸确定transform 只做视觉缩放不改变占位
注意不可加 object-fit: containtransformFor 的非均匀 scale 依赖位图填满
布局盒这一前提才能恰好抵消失真素材网格的缩略图容器恒为正方形与原图
宽高比不同contain letterbox 会导致非均匀 scale 把图压扁 */
.lightbox-img {
position: absolute;
left: 0;
@ -21,7 +24,6 @@
transform-origin: top left;
max-width: none;
max-height: none;
object-fit: contain;
opacity: 1;
}