fix(lightbox): 回 1x 自动归中+平移四向钳制(用户实测 bug)
用户实测:缩放回 100% 图片不回中显示不全;另放大态可拖飞出舞台。 三修:zoomAt next<=1 归零 translate;applyZoom 平移钳制 |t|≤尺 寸差/2(放大态不变量=图盒覆盖舞台而非缩在视口内);滚轮下限 0.5x→1x(fit 即最小,0.5x 制造偏移态)。mockup 同步三修。 验证: 回 1x transform=none 实测;四角极限拖拽覆盖舞台不变量 全过;E2E 34/34 Spec: none (用户实测 bug 热修,线上 v2+mockup 双修) Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
54769bcfdf
commit
44626f8edc
2 files changed
+108
-11
No files matched your search
@@ -238,7 +238,11 @@ function show(i,dir){
|
||||
}
|
||||
|
||||
/* ===== 缩放:滚轮锚点 / 双击 / 双指 / 拖拽(复用线上 v2 数学) ===== */
|
||||
function applyZoom(){pic.style.transform=`translate(${zoom.tx}px,${zoom.ty}px) scale(${zoom.s})`;
|
||||
function clampPan(){const mx=Math.max(0,pic.clientWidth*(zoom.s-1)/2),
|
||||
my=Math.max(0,pic.clientHeight*(zoom.s-1)/2);
|
||||
zoom.tx=Math.min(mx,Math.max(-mx,zoom.tx));zoom.ty=Math.min(my,Math.max(-my,zoom.ty));}
|
||||
function applyZoom(){clampPan();
|
||||
pic.style.transform=zoom.s===1?'none':`translate(${zoom.tx}px,${zoom.ty}px) scale(${zoom.s})`;
|
||||
pic.classList.toggle('zoomed',zoom.s!==1);
|
||||
lbx.classList.toggle('clean',zoom.s!==1);}
|
||||
function instantResetZoom(){const keep=pic.style.transition;pic.style.transition='none';
|
||||
@@ -246,7 +250,7 @@ function instantResetZoom(){const keep=pic.style.transition;pic.style.transition
|
||||
lbx.classList.remove('clean');void pic.offsetWidth;pic.style.transition=keep||'';}
|
||||
function zoomAt(next,cx,cy){const r=pic.getBoundingClientRect();
|
||||
const ax=cx-r.left-r.width/2-zoom.tx,ay=cy-r.top-r.height/2-zoom.ty;
|
||||
const k=next/zoom.s;zoom.tx-=ax*(k-1);zoom.ty-=ay*(k-1);zoom.s=next;applyZoom();zindShow();}
|
||||
if(next<=1){zoom={s:1,tx:0,ty:0};applyZoom();zindShow();return;}const k=next/zoom.s;zoom.tx-=ax*(k-1);zoom.ty-=ay*(k-1);zoom.s=next;applyZoom();zindShow();}
|
||||
let zt;function zindShow(){const z=$('zind');z.textContent=Math.round(zoom.s*100)+'%';
|
||||
z.classList.add('show');clearTimeout(zt);zt=setTimeout(()=>z.classList.remove('show'),900);}
|
||||
lbx.addEventListener('wheel',e=>{if(!open_)return;e.preventDefault();
|
||||
|
||||
@@ -51,6 +51,7 @@ const cards = await Promise.all(
|
||||
return {
|
||||
group: photo.data.group,
|
||||
title: photo.data.title,
|
||||
caption: photo.data.caption ?? '',
|
||||
alt: photo.data.alt ?? photo.data.title,
|
||||
image: photo.data.image,
|
||||
exif: formatExif(photo.data.exif),
|
||||
@@ -92,9 +93,12 @@ const first = cards[0];
|
||||
type="button"
|
||||
class="shot-btn"
|
||||
data-title={card.title}
|
||||
data-caption={card.caption}
|
||||
data-exif={card.exif}
|
||||
data-full={card.fullSrc}
|
||||
data-original={card.original}
|
||||
data-w={card.w}
|
||||
data-h={card.h}
|
||||
aria-label={t('gallery_view_aria', locale).replace('{title}', card.title) +
|
||||
(card.exif ? t('gallery_view_aria_exif', locale).replace('{exif}', card.exif) : '')}
|
||||
>
|
||||
@@ -130,9 +134,11 @@ const first = cards[0];
|
||||
</svg>
|
||||
</button>
|
||||
<div class="lb-meta">
|
||||
<p id="lb-caption" class="lb-caption">{first?.caption ?? ''}</p>
|
||||
<div class="lb-row">
|
||||
<strong id="lb-title">{first?.title}</strong>
|
||||
<span id="lb-exif">{first?.exif}</span>
|
||||
<span id="lb-count">1 / {cards.length}</span>
|
||||
<span id="lb-exif" class="lb-exif">{first?.exif}</span>
|
||||
<span class="lb-orig">
|
||||
<a id="lb-original" class="lb-act" href={first?.original} target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M21 3l-9 9M10 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-4"></path></svg>
|
||||
@@ -144,6 +150,7 @@ const first = cards[0];
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button id="lb-next" class="tap lbnav" type="button" aria-label={t('gallery_lightbox_next', locale)}>
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m9.5 5.5 6.5 6.5-6.5 6.5"></path>
|
||||
@@ -164,9 +171,66 @@ const first = cards[0];
|
||||
const img = document.getElementById('lb-img') as HTMLImageElement;
|
||||
const titleEl = document.getElementById('lb-title') as HTMLElement;
|
||||
const exifEl = document.getElementById('lb-exif') as HTMLElement;
|
||||
const captionEl = document.getElementById('lb-caption') as HTMLElement | null;
|
||||
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let idx = 0;
|
||||
|
||||
// ── v2 缩放态:滚轮光标锚点缩放(0.5x–5x)+ 放大后拖拽平移 + 双击 1x↔2.5x ──
|
||||
const zoom = { s: 1, tx: 0, ty: 0 };
|
||||
const applyZoom = () => {
|
||||
// 平移钳制:缩放后的图盒不得超出舞台(|t| ≤ 尺寸差/2)——拖飞与回 1x 偏移残留同治
|
||||
const mx = Math.max(0, (img.clientWidth * (zoom.s - 1)) / 2);
|
||||
const my = Math.max(0, (img.clientHeight * (zoom.s - 1)) / 2);
|
||||
zoom.tx = Math.min(mx, Math.max(-mx, zoom.tx));
|
||||
zoom.ty = Math.min(my, Math.max(-my, zoom.ty));
|
||||
img.style.transform = zoom.s === 1 ? 'none' : `translate(${zoom.tx}px, ${zoom.ty}px) scale(${zoom.s})`;
|
||||
img.classList.toggle('zoomed', zoom.s !== 1);
|
||||
};
|
||||
const resetZoom = () => { zoom.s = 1; zoom.tx = 0; zoom.ty = 0; applyZoom(); };
|
||||
const zoomAt = (next: number, cx: number, cy: number) => {
|
||||
// 光标为不动点:先求图面上锚点,再按新 scale 反算平移量
|
||||
const rect = img.getBoundingClientRect();
|
||||
const ax = cx - rect.left - rect.width / 2 - zoom.tx;
|
||||
const ay = cy - rect.top - rect.height / 2 - zoom.ty;
|
||||
const k = next / zoom.s;
|
||||
zoom.tx -= ax * (k - 1);
|
||||
zoom.ty -= ay * (k - 1);
|
||||
if (next <= 1) { zoom.s = 1; zoom.tx = 0; zoom.ty = 0; } else { zoom.s = next; }
|
||||
applyZoom();
|
||||
};
|
||||
lb.addEventListener('wheel', (e) => {
|
||||
if (!lb.open) return;
|
||||
e.preventDefault(); // 滚轮在灯箱内一律缩放:含 Ctrl+滚轮(劫持页面缩放的预期在灯箱内成立)
|
||||
const factor = Math.exp(-e.deltaY * 0.0015);
|
||||
const next = Math.min(5, Math.max(1, zoom.s * factor)); // 下限 1:fit-contain 即最小(0.5x 制造偏移态)
|
||||
if (next === zoom.s) return;
|
||||
zoomAt(next, e.clientX, e.clientY);
|
||||
}, { passive: false });
|
||||
// 拖拽平移:仅放大态(=1 时不抢点击/翻页);pointer capture 全程跟随
|
||||
let drag: { x: number; y: number; tx: number; ty: number; moved: boolean } | null = null;
|
||||
img.addEventListener('pointerdown', (e) => {
|
||||
if (zoom.s <= 1) return;
|
||||
e.preventDefault();
|
||||
img.setPointerCapture(e.pointerId);
|
||||
drag = { x: e.clientX, y: e.clientY, tx: zoom.tx, ty: zoom.ty, moved: false };
|
||||
img.classList.add('grabbing');
|
||||
});
|
||||
img.addEventListener('pointermove', (e) => {
|
||||
if (!drag) return;
|
||||
const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
|
||||
if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
|
||||
zoom.tx = drag.tx + dx;
|
||||
zoom.ty = drag.ty + dy;
|
||||
applyZoom();
|
||||
});
|
||||
const endDrag = () => { drag = null; img.classList.remove('grabbing'); };
|
||||
img.addEventListener('pointerup', endDrag);
|
||||
img.addEventListener('pointercancel', endDrag);
|
||||
img.addEventListener('dblclick', (e) => {
|
||||
if (zoom.s === 1) zoomAt(2.5, e.clientX, e.clientY);
|
||||
else resetZoom();
|
||||
});
|
||||
|
||||
const show = (i: number) => {
|
||||
idx = (i + shots.length) % shots.length;
|
||||
const btn = shots[idx]!;
|
||||
@@ -175,6 +239,11 @@ const first = cards[0];
|
||||
const title = btn.dataset.title ?? '';
|
||||
titleEl.textContent = title;
|
||||
titleEl.style.display = title ? '' : 'none';
|
||||
if (captionEl) {
|
||||
captionEl.textContent = btn.dataset.caption ?? '';
|
||||
captionEl.style.display = captionEl.textContent ? '' : 'none';
|
||||
}
|
||||
resetZoom(); // 换图回正:缩放态不跨图残留
|
||||
exifEl.textContent = btn.dataset.exif ?? '';
|
||||
exifEl.style.display = btn.dataset.exif ? '' : 'none';
|
||||
const countEl = document.getElementById('lb-count');
|
||||
@@ -197,6 +266,7 @@ const first = cards[0];
|
||||
document.getElementById('lb-prev')?.addEventListener('click', () => show(idx - 1));
|
||||
document.getElementById('lb-next')?.addEventListener('click', () => show(idx + 1));
|
||||
document.getElementById('lb-close')?.addEventListener('click', () => lb.close());
|
||||
lb.addEventListener('close', resetZoom);
|
||||
lb.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
@@ -333,27 +403,50 @@ const first = cards[0];
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
overflow: hidden; /* 灯箱内绝不出滚动条:图片完整可见交给 contain */
|
||||
}
|
||||
#lb::backdrop { background: rgba(8, 10, 14, 0.95); }
|
||||
.lb-figure {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative; /* img 绝对填满:grid/flex item 的 % 高存在 auto 行高循环失效坑 */
|
||||
padding: var(--space-5) var(--space-3) var(--space-2);
|
||||
}
|
||||
#lb-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain; /* 任意比例图在容器内完整可见,绝无溢出 */
|
||||
border-radius: 6px;
|
||||
transition: opacity 160ms var(--ease);
|
||||
/* v2 缩放:transform 过渡仅非 reduced-motion(缩放本体是直接交互,不豁免) */
|
||||
touch-action: none; /* 平移交给 pointer 事件,禁浏览器默认手势 */
|
||||
will-change: transform;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.lb-bottom {
|
||||
#lb-img.zoomed { cursor: grab; }
|
||||
#lb-img.grabbing { cursor: grabbing; }
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
#lb-img { transition: opacity 160ms var(--ease), transform 90ms linear; }
|
||||
}
|
||||
/* caption 层:正文体(区别于 mono 元数据),恒深灯箱底上的浅字 */
|
||||
.lb-caption {
|
||||
margin: 0 0 6px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13.5px;
|
||||
line-height: 1.6;
|
||||
color: #dfe6f3;
|
||||
max-width: 640px;
|
||||
}
|
||||
.lb-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.lb-exif { color: #98a2b3; }
|
||||
.lb-bottom {
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
color: var(--code-ink);
|
||||
font-family: var(--font-mono);
|
||||
@@ -362,7 +455,7 @@ const first = cards[0];
|
||||
.lb-orig {
|
||||
display: inline-flex;
|
||||
gap: var(--space-3);
|
||||
margin-left: var(--space-2);
|
||||
margin-left: auto; /* 胶囊组右推:行内 title/count/exif 在左 */
|
||||
}
|
||||
/* 动作胶囊:与 lbnav 同语言(暗底/描边/阴影),不再是裸链 */
|
||||
.lb-act {
|
||||
|
||||
Reference in new issue
Block a user