diff --git a/src/components/pages/GalleryPage.astro b/src/components/pages/GalleryPage.astro index 16b3c4c..d6de018 100644 --- a/src/components/pages/GalleryPage.astro +++ b/src/components/pages/GalleryPage.astro @@ -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]; 1 / {cards.length} - +

{first?.caption ?? ''}

+
{first?.title} {first?.exif} @@ -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 之外) */ diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index d485120..a2a9486 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -107,6 +107,7 @@ const dict = { // ===== F 车道:访问量小字(Artalk pv,组件内独立计数) ===== pv_viewed: { zh: '人看过', en: 'views' }, lb_hint: { zh: '滚轮缩放 · 双击放大 · Esc 关闭', en: 'Wheel to zoom · Double-click · Esc' }, + lb_hint_touch: { zh: '双指缩放 · 双击放大', en: 'Pinch to zoom · Double-tap' }, // ===== 命令面板(Phase 4 T1 车道):动作/导航/分组与键位提示 ===== nav_home: { zh: '首页', en: 'Home' },