feat(gallery): 灯箱 v2.2 定版——一律侧栏 200px+长文策略+六条 polish
布局统一(视觉轨像素实测裁定 A 改窄变体:侧栏方案让照片比底 栏方案大 39%/22%):≥820 全比例右侧栏 200px(舞台恰 1080×720 零信箱)、竖横顶部统一 26px;<820 底部抽屉。EXIF 两行折行+分段 span(字段级 nowrap+::before 绑后段符号——一刀治悬空/断点丢符/ 间距不对称三症状);caption clamp 4 行+展开(桌面 60dvh/移动 40dvh 内滚,锁 --fig-h 保证展开前后图区几何不变);移动底条 flex 复活+箭头同排+EXIF 两行;提示胶囊移动挂顶+触屏文案;820 计数间隙 14px;全量左对齐(孤字居中终止)。 车道:op-artalk(专项 8/8×3 轮+E2E 37/37);视觉轨道三轮裁定 (需修→复检→定版),EXIF 间距自验 5.2px 对称、换行对 1(镜头 名独占行,非缺陷) 验证: E2E 37/37;EXIF 三段宽 measured(5.2px 对称/符号 ::before /无悬空);移动 flex+提示顶挂实测;五档几何(1280 横竖/820/768/390) Spec: none (灯箱 v2.2 定版,车道级交付) Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
3b951d3de6
commit
0e06c25f67
2 files changed
+193
-37
No files matched your search
@@ -23,10 +23,13 @@ function formatExif(exif: PhotoExif | undefined): string {
|
||||
: exif.exposureTime >= 1
|
||||
? `${exif.exposureTime}s`
|
||||
: `1/${Math.round(1 / exif.exposureTime)}s`;
|
||||
// 镜头名常内嵌光圈档位(如「28-200mm F2.8-5.6 A071」):此时不再追加 fNumber,
|
||||
// 否则同一条里出现两组光圈读数(视觉终审 ①)
|
||||
const lensHasAperture = !!exif.lens && /[fF][//]?\d/.test(exif.lens);
|
||||
return [
|
||||
[exif.make, exif.model].filter(Boolean).join(' ') || undefined,
|
||||
exif.lens,
|
||||
exif.fNumber === undefined ? undefined : `f/${exif.fNumber}`,
|
||||
exif.fNumber === undefined || lensHasAperture ? undefined : `f/${exif.fNumber}`,
|
||||
shutter,
|
||||
exif.iso === undefined ? undefined : `ISO ${exif.iso}`,
|
||||
]
|
||||
@@ -120,7 +123,10 @@ const first = cards[0];
|
||||
|
||||
<dialog id="lb" aria-label={t('gallery_lightbox_label', locale)}>
|
||||
<span id="lb-count" class="lb-count lb-l1">1 / {cards.length}</span>
|
||||
<p id="lb-hint" class="lb-hint lb-l1" hidden>{t('lb_hint', locale)}</p>
|
||||
<p id="lb-hint" class="lb-hint lb-l1" hidden>
|
||||
<span class="hint-desktop">{t('lb_hint', locale)}</span>
|
||||
<span class="hint-touch">{t('lb_hint_touch', locale)}</span>
|
||||
</p>
|
||||
<span id="lb-zoombadge" class="lb-zoombadge" aria-hidden="true"></span>
|
||||
<button id="lb-close" class="tap lbnav lb-l1" type="button" aria-label={t('gallery_lightbox_close', locale)}>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
||||
@@ -138,6 +144,14 @@ const first = cards[0];
|
||||
</button>
|
||||
<div class="lb-meta lb-l2">
|
||||
<p id="lb-caption" class="lb-caption">{first?.caption ?? ''}</p>
|
||||
<button
|
||||
id="lb-cap-more"
|
||||
class="lb-cap-more"
|
||||
type="button"
|
||||
data-more={locale === 'en' ? 'More' : '展开'}
|
||||
data-less={locale === 'en' ? 'Less' : '收起'}
|
||||
hidden
|
||||
>{locale === 'en' ? 'More' : '展开'}</button>
|
||||
<div class="lb-row">
|
||||
<strong id="lb-title">{first?.title}</strong>
|
||||
<span id="lb-exif" class="lb-exif">{first?.exif}</span>
|
||||
@@ -386,14 +400,7 @@ const first = cards[0];
|
||||
}
|
||||
}
|
||||
|
||||
// ── ⑧ 长图侧置 meta:竖图(ratio<0.9)且视口≥820 ──
|
||||
function applySideMeta() {
|
||||
const btn = shots[idx];
|
||||
if (!btn) return;
|
||||
const w = Number(btn.dataset.w) || 1, h = Number(btn.dataset.h) || 1;
|
||||
lb.classList.toggle('side-meta', w / h < 0.9 && innerWidth >= 820);
|
||||
}
|
||||
addEventListener('resize', () => lb.open && applySideMeta(), { passive: true });
|
||||
// ⑧(v2.2 退役)比例判定→一律侧栏由 CSS 媒体查询接管,见样式段 820px 块
|
||||
|
||||
// ── ④ 相邻预载(克制档 ±1,idle 后) ──
|
||||
const idle: (f: () => void) => void =
|
||||
@@ -443,6 +450,40 @@ const first = cards[0];
|
||||
setTimeout(() => img.classList.remove('slide-in'), 320);
|
||||
}
|
||||
|
||||
// caption 长文 clamp(v2.2):窄栏 4 行截断,溢出才现「展开」钮;换图复位
|
||||
const capMore = document.getElementById('lb-cap-more') as HTMLButtonElement | null;
|
||||
function resetCaptionClamp() {
|
||||
if (!captionEl || !capMore) return;
|
||||
// 先施加 clamp 再测:未 clamp 时 scrollHeight==clientHeight 恒不溢出(顺序反了会永不出钮)
|
||||
captionEl.classList.add('clamp');
|
||||
capMore.hidden = true;
|
||||
capMore.textContent = capMore.dataset.more ?? capMore.textContent;
|
||||
requestAnimationFrame(() => {
|
||||
if (!captionEl) return;
|
||||
if (captionEl.scrollHeight <= captionEl.clientHeight + 2) {
|
||||
captionEl.classList.remove('clamp'); // 短 caption 不截断
|
||||
} else {
|
||||
capMore.hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
capMore?.addEventListener('click', () => {
|
||||
const figEl = lb.querySelector('.lb-figure') as HTMLElement | null;
|
||||
const willExpand = captionEl?.classList.contains('clamp') ?? false;
|
||||
// 锁高必须在切换 clamp 之前量:切完 caption 立刻变高、figure 已被挤压(量到错误值)
|
||||
if (willExpand && figEl) {
|
||||
lb.style.setProperty('--fig-h', `${Math.round(figEl.getBoundingClientRect().height)}px`);
|
||||
}
|
||||
const clamped = captionEl?.classList.toggle('clamp');
|
||||
capMore.textContent = clamped ? (capMore.dataset.more ?? '展开') : (capMore.dataset.less ?? '收起');
|
||||
lb.classList.toggle('cap-open', !clamped);
|
||||
if (!clamped) {
|
||||
captionEl?.scrollTo?.({ top: 0 });
|
||||
} else {
|
||||
lb.style.removeProperty('--fig-h');
|
||||
}
|
||||
});
|
||||
|
||||
const show = (i: number) => {
|
||||
const n = shots.length;
|
||||
const prevIdx = idx;
|
||||
@@ -459,8 +500,22 @@ const first = cards[0];
|
||||
captionEl.textContent = btn.dataset.caption ?? '';
|
||||
captionEl.style.display = captionEl.textContent ? '' : 'none';
|
||||
}
|
||||
exifEl.textContent = btn.dataset.exif ?? '';
|
||||
exifEl.style.display = btn.dataset.exif ? '' : 'none';
|
||||
// EXIF 字段级渲染:每段 nowrap(分隔符由 CSS ::after 粘在段尾,永不悬空行首/行尾)
|
||||
const exifText = btn.dataset.exif ?? '';
|
||||
exifEl.replaceChildren(
|
||||
...exifText
|
||||
.split(' · ')
|
||||
.filter(Boolean)
|
||||
.map((seg) => {
|
||||
const s = document.createElement('span');
|
||||
s.className = 'exif-seg';
|
||||
s.textContent = seg;
|
||||
return s;
|
||||
}),
|
||||
);
|
||||
exifEl.style.display = exifText ? '' : 'none';
|
||||
exifEl.title = exifText; // 窄栏两行折行后全串仍可悬停查看
|
||||
resetCaptionClamp();
|
||||
const countEl = document.getElementById('lb-count');
|
||||
if (countEl) countEl.textContent = `${idx + 1} / ${n}`;
|
||||
const orig = btn.dataset.original ?? '';
|
||||
@@ -469,11 +524,11 @@ const first = cards[0];
|
||||
dl.href = orig;
|
||||
dl.setAttribute('download', btn.dataset.title ?? 'photo');
|
||||
resetZoom();
|
||||
applySideMeta();
|
||||
if (firstOpen) {
|
||||
// ① 起飞:先摆 contain 真身再克隆从缩略图矩形起飞盖过渡
|
||||
img.src = btn.dataset.full ?? '';
|
||||
lb.classList.remove('chrome-in', 'chrome-off');
|
||||
lb.classList.remove('chrome-in', 'chrome-off', 'cap-open');
|
||||
lb.style.removeProperty('--fig-h');
|
||||
lb.showModal();
|
||||
requestAnimationFrame(() => lb.classList.add('chrome-in'));
|
||||
const thumb = btn.querySelector('img');
|
||||
@@ -494,7 +549,8 @@ const first = cards[0];
|
||||
lb.addEventListener('close', () => {
|
||||
flyBack(); // ① 回落到当前缩略图(实时矩形,列表滚动后仍准)
|
||||
resetZoom();
|
||||
lb.classList.remove('chrome-in', 'chrome-off', 'side-meta');
|
||||
lb.classList.remove('chrome-in', 'chrome-off', 'cap-open');
|
||||
lb.style.removeProperty('--fig-h');
|
||||
});
|
||||
// 触屏快滑翻页(v2 保持):横滑翻页与 ⑩ 竖拖关正交
|
||||
let x0: number | null = null;
|
||||
@@ -668,6 +724,9 @@ const first = cards[0];
|
||||
}
|
||||
.lb-exif { color: #98a2b3; }
|
||||
.lb-bottom {
|
||||
display: flex; /* 视觉终审 ③:此前缺 flex,order/flex-basis 全是死代码 */
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
color: var(--code-ink);
|
||||
font-family: var(--font-mono);
|
||||
@@ -702,10 +761,11 @@ const first = cards[0];
|
||||
.lb-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
text-align: left; /* 视觉终审 ①:窄栏/移动条统一左对齐,长文孤字不再居中 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.lb-meta { text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); }
|
||||
.lb-meta strong {
|
||||
@@ -716,10 +776,18 @@ const first = cards[0];
|
||||
}
|
||||
.lb-meta span {
|
||||
opacity: 0.82;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* EXIF 分段(视觉终审 ②):每段 nowrap、分隔符 ::after 粘在段尾——
|
||||
换行只发生在段间,`·` 永不悬空行首/行尾;容器允许两行折行 */
|
||||
.lb-exif { white-space: normal; }
|
||||
/* EXIF 分隔符一刀修三(视觉终审复检方案):`·` 绑到**后一段**作 ::before 前缀并禁止其后断行——
|
||||
悬空行尾消失、断点处符号不丢、两侧间距对称(.45em / .45em,此前 19-20px vs 3-4px)。
|
||||
段为 JS 动态创建,仍须 :global(scoped 够不着,同类坑第四次的延续)。 */
|
||||
:global(.exif-seg) { display: inline-block; white-space: nowrap; }
|
||||
:global(.exif-seg + .exif-seg) { margin-left: 0.45em; }
|
||||
:global(.exif-seg + .exif-seg)::before { content: '·'; margin-right: 0.45em; opacity: 0.55; }
|
||||
/* 计数保持单行 */
|
||||
.lb-count { white-space: nowrap; }
|
||||
.lbnav {
|
||||
flex: none;
|
||||
width: 44px;
|
||||
@@ -737,10 +805,14 @@ const first = cards[0];
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.lb-bottom { flex-wrap: wrap; justify-content: center; }
|
||||
.lb-orig { margin-left: 0; }
|
||||
.lb-meta { order: 3; flex-basis: 100%; }
|
||||
@media (max-width: 819px) {
|
||||
/* 触屏条:‹ 图 › 一行,caption/meta 整行在下(flex 生效后 order 才有意义) */
|
||||
.lb-bottom { flex-wrap: wrap; justify-content: flex-start; align-items: center; }
|
||||
.lb-orig { margin-left: auto; }
|
||||
.lb-meta { order: 3; flex-basis: 100%; }
|
||||
.lb-meta strong, .lb-meta .lb-caption { text-align: left; }
|
||||
/* 移动端 EXIF 允许两行折行(撤 nowrap/ellipsis 截断) */
|
||||
.lb-exif { white-space: normal; }
|
||||
}
|
||||
|
||||
/* ── v2.1 chrome 双层淡入(③):层1 +140ms、层2 +300ms,仅 opacity 无位移 ── */
|
||||
@@ -768,7 +840,8 @@ const first = cards[0];
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* 首开提示胶囊(⑦):底部居中 meta 上方,2.6s 自隐由 JS 控制 */
|
||||
/* 首开提示胶囊(⑦):桌面底部居中 meta 上方(移动挂顶部,见下段),2.6s 自隐 */
|
||||
.lb-hint .hint-touch { display: none; }
|
||||
.lb-hint {
|
||||
position: absolute;
|
||||
bottom: 118px;
|
||||
@@ -802,20 +875,102 @@ const first = cards[0];
|
||||
}
|
||||
.lb-zoombadge.on { opacity: 0.9; }
|
||||
|
||||
/* 长图侧置 meta(⑧):竖图宽视口时层2 挪进右侧 letterbox,横图维持底部 */
|
||||
#lb.side-meta .lb-l2 {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
translate: 0 -50%;
|
||||
width: 216px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-left: 16px;
|
||||
/* ── v2.2 一律侧栏(方案 A 窄变体,视觉裁定 §4):桌面 ≥820 全比例信息进右侧 200px 栏,
|
||||
舞台恰 1080×720(3:2 横图零信箱);竖/横内容顶统一 26px(figure 高=视口-54,顶 26 → 两种比例
|
||||
内容顶都落在 26px,不再一贴顶一留白)。移动 <820 维持底部条(两种形态是裁定接受的代价) ── */
|
||||
@media (min-width: 820px) {
|
||||
#lb .lb-figure { padding: 0; }
|
||||
/* 舞台 = (1280-200) × (视口-26-54) = 1080×720(3:2 横图零信箱)。
|
||||
替换元素(img)absolute 下 width:auto 取 intrinsic 宽而非 left/right 约束——须显式 calc */
|
||||
#lb #lb-img {
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: 26px;
|
||||
bottom: auto;
|
||||
width: calc(100% - 200px);
|
||||
height: calc(100dvh - 80px);
|
||||
}
|
||||
/* 底部 54px 视觉余量:让 figure 高=视口-26-54,内容顶恒 26 */
|
||||
#lb .lb-bottom { padding: 0; min-height: 54px; display: block; }
|
||||
#lb .lb-l2 {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
translate: none;
|
||||
width: 200px;
|
||||
max-height: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 0 18px 24px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
#lb .lb-l2 .lb-row { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
#lb .lb-l2 .lb-orig { margin-left: 0; }
|
||||
/* 200px 栏宽:EXIF 允许两行折行(裁定补丁 3:不再硬裁「…A071 · 1」) */
|
||||
#lb .lb-exif { white-space: normal; font-size: 11.5px; line-height: 1.5; color: #98a2b3; }
|
||||
/* 计数与关闭钮落栏顶(栏内容底部对齐,顶部空区做面板头) */
|
||||
/* 视觉终审 ⑤:计数与 × 间隙 ≥6px(62px 时实测仅 1px) */
|
||||
#lb .lb-count { right: 74px; }
|
||||
}
|
||||
#lb.side-meta .lb-l2 .lb-row { flex-direction: column; align-items: flex-start; }
|
||||
|
||||
/* caption 长文策略(v2.2 追加):折叠 clamp 桌面 4 行 / 移动 2 行;
|
||||
展开态给 max-height 上限 + 内部细滚动,数百字也不推爆舞台(图区几何不变量) */
|
||||
.lb-caption {
|
||||
white-space: pre-wrap; /* 段落感:描述里的换行原样渲染 */
|
||||
}
|
||||
.lb-caption.clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
.lb-caption:not(.clamp) {
|
||||
max-height: 60dvh;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
line-height: 1.7;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (max-width: 819px) {
|
||||
.lb-caption.clamp { -webkit-line-clamp: 2; }
|
||||
.lb-caption:not(.clamp) { max-height: 40dvh; }
|
||||
/* 移动端展开态:底部条浮层化(出流),图区 flex 尺寸展开前后不变——
|
||||
这是长描述的关键不变量(桌面侧栏天然成立,移动须显式出流) */
|
||||
#lb.cap-open .lb-figure { height: var(--fig-h, auto); flex: none; }
|
||||
/* 视觉终审 ④:移动提示胶囊挂顶部(bottom:118px 会压在底部条/EXIF 上),
|
||||
文案切触屏手势(滚轮在手机上不存在) */
|
||||
.lb-hint { bottom: auto; top: 76px; white-space: normal; max-width: calc(100% - 32px); text-align: center; }
|
||||
.lb-hint .hint-desktop { display: none; }
|
||||
.lb-hint .hint-touch { display: inline; }
|
||||
#lb.cap-open .lb-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(8, 10, 14, 0.92);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.lb-cap-more {
|
||||
align-self: flex-start;
|
||||
margin: 2px 0 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #98a2b3;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
text-decoration: underline dotted;
|
||||
cursor: pointer;
|
||||
}
|
||||
.lb-cap-more:hover { color: #dfe6f3; }
|
||||
|
||||
/* FLIP 起飞/回落克隆(①):dialog 内 absolute(dialog 全屏故 viewport 坐标等价);
|
||||
关闭回落克隆挂 body 需 fixed(dialog 关闭即离 top-layer,回落动画要活到 dialog 之外) */
|
||||
|
||||
Reference in new issue
Block a user