feat(i18n): UI 字典串接与三页薄化——/en/ 全站落成
- op-gitea 车道:ui.ts 扩 26 键(占位 {n}/{title}/{exif} 约定),五组件
locale prop 串接,zh 零回归(48 键/18 页实测)
- orchestrator 收尾:gallery/moments/about 三页 body 提取为
components/pages/*Page(zh/en 薄页双路由),/en/ 全家福 21 页
- 提取工程三坑入库:HTML 注释不可进 frontmatter 模块区;搬移脚本会吃
开栏 ---;单级页面搬二级目录后 import 全线差一层
验证: astro build 21 页 exit 0;/en/ 七类路由产物断言过;en 页
ICP 备案保留;zh 侧文案抽查不变
Spec: 2026-10-25_add-blog-i18n
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
c74815b516
commit
4aa79fca06
13 files changed
+757
-653
No files matched your search
@@ -1,5 +1,13 @@
|
||||
---
|
||||
// 页脚:版权 + ICP 备案(法定保留)+ 构建透明槽位(T7 built-from 填充)
|
||||
import { t } from '../i18n/ui';
|
||||
import type { Locale } from '../lib/locale';
|
||||
|
||||
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
const { locale = 'zh' } = Astro.props;
|
||||
---
|
||||
|
||||
<footer class="site-footer safe-area">
|
||||
@@ -7,7 +15,7 @@
|
||||
<p class="fine">
|
||||
© {new Date().getFullYear()} wait4x ·
|
||||
<a href="https://beian.miit.gov.cn/" rel="noopener noreferrer" target="_blank" class="link">
|
||||
陕ICP备2026014860号-1
|
||||
{t('footer_icp', locale)}
|
||||
</a>
|
||||
</p>
|
||||
<!-- T7 注入:built from {commit} · {time} · Astro(链到 gitea commit) -->
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
---
|
||||
// 站内搜索(Pagefind):首次打开才懒加载索引(spectre 模式,见调研报告 §4c)
|
||||
// 入口:Header 搜索钮 + 键盘 `/`
|
||||
import { t } from '../i18n/ui';
|
||||
import type { Locale } from '../lib/locale';
|
||||
|
||||
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
const { locale = 'zh' } = Astro.props;
|
||||
---
|
||||
|
||||
<dialog id="search-dialog" class="search-dialog">
|
||||
<input id="search-input" type="search" placeholder="搜索文章…(Esc 关闭)" autocomplete="off" />
|
||||
<dialog id="search-dialog" class="search-dialog" data-locale={locale}>
|
||||
<input id="search-input" type="search" placeholder={t('search_placeholder', locale)} autocomplete="off" />
|
||||
<ul id="search-results" class="results" aria-live="polite"></ul>
|
||||
<p class="search-hint">索引由 Pagefind 构建期生成 · 静态站零后端</p>
|
||||
<p class="search-hint">{t('search_hint', locale)}</p>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
import { t } from '../i18n/ui';
|
||||
|
||||
// 模块脚本读不到 Astro props:空态文案语言在事件触发时从 dialog 的
|
||||
// data-locale 现取——ClientRouter 换页重建 DOM 后仍能拿到当前页语言
|
||||
const dlg = document.getElementById('search-dialog') as HTMLDialogElement;
|
||||
const input = document.getElementById('search-input') as HTMLInputElement;
|
||||
const list = document.getElementById('search-results')!;
|
||||
@@ -59,7 +71,10 @@
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
}
|
||||
if (!list.children.length) list.innerHTML = '<li class="empty">无结果</li>';
|
||||
if (!list.children.length) {
|
||||
const loc = (document.getElementById('search-dialog')?.dataset.locale ?? 'zh') as 'zh' | 'en';
|
||||
list.innerHTML = `<li class="empty">${t('search_no_results', loc)}</li>`;
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import IdCard from '../../components/IdCard.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
import type { Locale } from '../../lib/locale';
|
||||
|
||||
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
const { locale = 'zh' } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${t('about_title', locale)} · wait4x`}>
|
||||
<h1 class="page-title">{t('about_title', locale)}</h1>
|
||||
|
||||
<IdCard />
|
||||
|
||||
<section class="tracks" aria-label={t('about_tracks_title', locale)}>
|
||||
<h2 class="sec-title">{t('about_tracks_title', locale)}</h2>
|
||||
<ul>
|
||||
<li class="track-row">
|
||||
<span class="track-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 4h6a4 4 0 0 1 4 4v13a3 3 0 0 0-3-3H2z"></path>
|
||||
<path d="M22 4h-6a4 4 0 0 0-4 4v13a3 3 0 0 1 3-3h7z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<strong>{t('about_track_reading_name', locale)}</strong>
|
||||
<p>{t('about_track_reading_desc', locale)}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="track-row">
|
||||
<span class="track-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m4 5.5 6.5 6.5L4 18.5"></path>
|
||||
<path d="M12.5 18.5H20"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<strong>{t('about_track_tech_name', locale)}</strong>
|
||||
<p>{t('about_track_tech_desc', locale)}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="track-row">
|
||||
<span class="track-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
|
||||
<circle cx="12" cy="13" r="4"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<strong>{t('about_track_photo_name', locale)}</strong>
|
||||
<p>{t('about_track_photo_desc', locale)}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="build" aria-label={t('about_build_aria', locale)}>
|
||||
<h2 class="sec-title">{t('about_build_title', locale)}</h2>
|
||||
<p>{t('about_build_desc', locale)}</p>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
font-size: var(--text-h1);
|
||||
line-height: var(--leading-heading);
|
||||
color: var(--ink-1);
|
||||
margin-top: var(--space-5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.sec-title {
|
||||
font-size: 14px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--ink-3);
|
||||
letter-spacing: 0.08em;
|
||||
margin: var(--space-5) 0 var(--space-3);
|
||||
}
|
||||
.tracks ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.track-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.track-icon {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-soft);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.track-row strong {
|
||||
font-size: 15px;
|
||||
color: var(--ink-1);
|
||||
}
|
||||
.track-row p {
|
||||
font-size: 14px;
|
||||
color: var(--ink-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.build p {
|
||||
font-size: 15px;
|
||||
color: var(--ink-2);
|
||||
max-width: var(--measure);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,402 @@
|
||||
---
|
||||
import { Image, getImage } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
import type { Locale } from '../../lib/locale';
|
||||
import type { PhotoExif } from '../../lib/exif';
|
||||
|
||||
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
const { locale = 'zh' } = Astro.props;
|
||||
|
||||
const photos = await getCollection('photos');
|
||||
|
||||
// EXIF 参数条(机身/镜头/光圈/快门/ISO)构建期拼好整串,灯箱运行时只做文本替换
|
||||
function formatExif(exif: PhotoExif | undefined): string {
|
||||
if (!exif) return '';
|
||||
const shutter =
|
||||
exif.exposureTime === undefined
|
||||
? undefined
|
||||
: exif.exposureTime >= 1
|
||||
? `${exif.exposureTime}s`
|
||||
: `1/${Math.round(1 / exif.exposureTime)}s`;
|
||||
return [
|
||||
[exif.make, exif.model].filter(Boolean).join(' ') || undefined,
|
||||
exif.lens,
|
||||
exif.fNumber === undefined ? undefined : `f/${exif.fNumber}`,
|
||||
shutter,
|
||||
exif.iso === undefined ? undefined : `ISO ${exif.iso}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
/* masonry:行高/列距用容器宽百分比(0.92cqw/1.62cqw,在 864px 版心即 8px/14px 档),
|
||||
行高随断点等比缩放,构建期一份 row-span 在 2/3 列全断点精确成立——
|
||||
px 定值行高会让格高随列宽漂移,窄屏裁切失控 */
|
||||
const ROW = 0.0092;
|
||||
const GAP = 0.0162;
|
||||
const spanFor = (w: number, h: number, cols: number) =>
|
||||
Math.round(((1 - GAP * (cols - 1)) / (cols * (w / h)) + GAP) / (ROW + GAP));
|
||||
|
||||
const cards = await Promise.all(
|
||||
photos.map(async (photo) => {
|
||||
// 宽高比来源:photos loader 产出 ImageMetadata 的 width/height(orientation 已修正)
|
||||
const w = photo.data.image.width ?? 4;
|
||||
const h = photo.data.image.height ?? 3;
|
||||
const full = await getImage({ src: photo.data.image, width: 1440 });
|
||||
return {
|
||||
group: photo.data.group,
|
||||
title: photo.data.title,
|
||||
alt: photo.data.alt ?? photo.data.title,
|
||||
image: photo.data.image,
|
||||
exif: formatExif(photo.data.exif),
|
||||
fullSrc: full.src,
|
||||
// 原图走仓外 store(构建时独立 rsync 到 /photos-originals/),git 不背 196M
|
||||
original: `/photos-originals/${photo.id}.jpg`,
|
||||
w,
|
||||
h,
|
||||
s3: spanFor(w, h, 3),
|
||||
s2: spanFor(w, h, 2),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const first = cards[0];
|
||||
---
|
||||
|
||||
<BaseLayout title={`${t('gallery_title', locale)} · wait4x`}>
|
||||
<h1 class="page-title">{t('gallery_title', locale)}</h1>
|
||||
<div class="g-main">
|
||||
<fieldset class="filters">
|
||||
<legend class="sr-only">{t('gallery_filter_legend', locale)}</legend>
|
||||
<input class="f-input" type="radio" name="gallery-group" id="f-all" checked />
|
||||
<label class="tap chip" for="f-all">{t('gallery_filter_all', locale)}</label>
|
||||
<input class="f-input" type="radio" name="gallery-group" id="f-street" />
|
||||
<label class="tap chip" for="f-street">{t('gallery_filter_street', locale)}</label>
|
||||
<input class="f-input" type="radio" name="gallery-group" id="f-landscape" />
|
||||
<label class="tap chip" for="f-landscape">{t('gallery_filter_landscape', locale)}</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="stage">
|
||||
<ul class="wall">
|
||||
{cards.map((card) => (
|
||||
<li
|
||||
class="shot"
|
||||
data-group={card.group}
|
||||
style={`aspect-ratio:${card.w}/${card.h}; --s3:${card.s3}; --s2:${card.s2}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="shot-btn"
|
||||
data-title={card.title}
|
||||
data-exif={card.exif}
|
||||
data-full={card.fullSrc}
|
||||
data-original={card.original}
|
||||
aria-label={t('gallery_view_aria', locale).replace('{title}', card.title) +
|
||||
(card.exif ? t('gallery_view_aria_exif', locale).replace('{exif}', card.exif) : '')}
|
||||
>
|
||||
<Image
|
||||
src={card.image}
|
||||
alt={card.alt}
|
||||
widths={[320, 560, 720]}
|
||||
sizes="(min-width: 820px) 30vw, 46vw"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p class="g-note">{t('gallery_count', locale).replace('{n}', String(cards.length))}</p>
|
||||
</div>
|
||||
|
||||
<dialog id="lb" aria-label={t('gallery_lightbox_label', locale)}>
|
||||
<button id="lb-close" class="tap lbnav" 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">
|
||||
<path d="m5 5 14 14M19 5 5 19"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="lb-figure">
|
||||
<img id="lb-img" src={first?.fullSrc} alt={first?.alt ?? ''} />
|
||||
</div>
|
||||
<div class="lb-bottom">
|
||||
<button id="lb-prev" class="tap lbnav" type="button" aria-label={t('gallery_lightbox_prev', 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="m14.5 5.5-6.5 6.5 6.5 6.5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="lb-meta">
|
||||
<strong id="lb-title">{first?.title}</strong>
|
||||
<span id="lb-exif">{first?.exif}</span>
|
||||
<span class="lb-orig">
|
||||
<a id="lb-original" href={first?.original} target="_blank" rel="noopener">{t('gallery_lightbox_original', locale)}</a>
|
||||
<a id="lb-download" href={first?.original} download>{t('gallery_lightbox_download', locale)}</a>
|
||||
</span>
|
||||
</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>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
// 灯箱:原生 dialog,←→ 切换、Esc 原生关闭;触屏快滑翻页是键盘的等价物。
|
||||
// ClientRouter 下模块脚本只执行一次,接线挂在 astro:page-load 按当前 DOM 重装
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
const lb = document.getElementById('lb') as HTMLDialogElement | null;
|
||||
const shots = [...document.querySelectorAll<HTMLButtonElement>('.shot-btn')];
|
||||
if (!lb || shots.length === 0) return;
|
||||
|
||||
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 reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let idx = 0;
|
||||
|
||||
const show = (i: number) => {
|
||||
idx = (i + shots.length) % shots.length;
|
||||
const btn = shots[idx]!;
|
||||
img.src = btn.dataset.full ?? '';
|
||||
img.alt = btn.ariaLabel ?? '';
|
||||
titleEl.textContent = btn.dataset.title ?? '';
|
||||
exifEl.textContent = btn.dataset.exif ?? '';
|
||||
const orig = btn.dataset.original ?? '';
|
||||
(document.getElementById('lb-original') as HTMLAnchorElement).href = orig;
|
||||
const dl = document.getElementById('lb-download') as HTMLAnchorElement;
|
||||
dl.href = orig;
|
||||
dl.setAttribute('download', btn.dataset.title ?? 'photo');
|
||||
if (!reduced.matches) {
|
||||
img.style.opacity = '0';
|
||||
requestAnimationFrame(() => {
|
||||
img.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
if (!lb.open) lb.showModal();
|
||||
};
|
||||
|
||||
shots.forEach((btn, i) => btn.addEventListener('click', () => show(i)));
|
||||
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('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
show(idx - 1);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
show(idx + 1);
|
||||
}
|
||||
});
|
||||
let x0: number | null = null;
|
||||
lb.addEventListener('touchstart', (e) => {
|
||||
x0 = e.touches[0]!.clientX;
|
||||
}, { passive: true });
|
||||
lb.addEventListener('touchend', (e) => {
|
||||
if (x0 === null) return;
|
||||
const dx = e.changedTouches[0]!.clientX - x0;
|
||||
if (Math.abs(dx) > 40) show(idx + (dx < 0 ? 1 : -1));
|
||||
x0 = null;
|
||||
}, { passive: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 版心放行:图库加宽到 measure+176(丙路线 §3;BaseLayout 默认 720) */
|
||||
:global(main#main:has(.g-main)) {
|
||||
max-width: calc(var(--measure) + 176px);
|
||||
}
|
||||
.page-title {
|
||||
font-size: var(--text-h1);
|
||||
line-height: var(--leading-heading);
|
||||
color: var(--ink-1);
|
||||
margin-top: var(--space-5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* 分组筛选:纯 CSS :checked + :has,radio 原生键盘可达(方向键切换) */
|
||||
.filters {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: var(--space-3) 0 var(--space-4);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.sr-only,
|
||||
.f-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip {
|
||||
padding-inline: 13px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
color: var(--ink-3);
|
||||
cursor: pointer;
|
||||
transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||
}
|
||||
.chip:hover { color: var(--ink-1); border-color: var(--ink-4); }
|
||||
.f-input:focus-visible + .chip { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.f-input:checked + .chip { color: var(--ink-1); border-color: var(--ink-3); font-weight: 500; }
|
||||
.g-main:has(#f-street:checked) .shot:not([data-group='street']) { display: none; }
|
||||
.g-main:has(#f-landscape:checked) .shot:not([data-group='landscape']) { display: none; }
|
||||
|
||||
/* masonry:stage 是容器查询锚,行高/列距以 stage 宽为基(cqw) */
|
||||
.stage { container-type: inline-size; }
|
||||
.wall {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-rows: 0.92cqw; /* 8px 档 @ 864px 版心 */
|
||||
grid-auto-flow: dense;
|
||||
gap: 1.62cqw; /* 14px 档 @ 864px 版心 */
|
||||
}
|
||||
@media (min-width: 820px) {
|
||||
.wall { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
.shot {
|
||||
grid-row-end: span var(--s2, 10);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (min-width: 820px) {
|
||||
.shot { grid-row-end: span var(--s3, 12); }
|
||||
}
|
||||
.shot-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.shot-btn :global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.g-note {
|
||||
margin-top: var(--space-3);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
/* 灯箱:全屏原生 dialog,高度用 dvh(规避 100vh 移动端地址栏陷阱) */
|
||||
#lb {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
#lb::backdrop { background: rgba(8, 10, 14, 0.95); }
|
||||
.lb-figure {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-5) var(--space-4) var(--space-2);
|
||||
}
|
||||
#lb-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
transition: opacity 160ms var(--ease);
|
||||
}
|
||||
.lb-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
color: var(--code-ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.lb-orig {
|
||||
display: inline-flex;
|
||||
gap: var(--space-3);
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
.lb-orig a {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
.lb-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.lb-meta strong {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
}
|
||||
.lb-meta span {
|
||||
opacity: 0.82;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lbnav {
|
||||
flex: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #e6e9ee;
|
||||
}
|
||||
.lbnav:hover { background: rgba(255, 255, 255, 0.12); }
|
||||
#lb-close {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.lb-bottom { flex-wrap: wrap; }
|
||||
.lb-orig {
|
||||
display: inline-flex;
|
||||
gap: var(--space-3);
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
.lb-orig a {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
.lb-meta { order: 3; flex-basis: 100%; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
import type { Locale } from '../../lib/locale';
|
||||
|
||||
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
|
||||
interface Props {
|
||||
locale?: Locale;
|
||||
}
|
||||
const { locale = 'zh' } = Astro.props;
|
||||
|
||||
const moments = (await getCollection('moments')).sort(
|
||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||
);
|
||||
const iso = (d: Date) => d.toISOString();
|
||||
---
|
||||
|
||||
<BaseLayout title={`${t('moments_title', locale)} · wait4x`}>
|
||||
<h1 class="page-title">{t('moments_title', locale)}</h1>
|
||||
<p class="page-note">{t('moments_note', locale)}</p>
|
||||
<ol class="tl">
|
||||
{moments.map((m) => (
|
||||
<li class="tl-item">
|
||||
<time class="tl-date" datetime={iso(m.data.pubDate)}>
|
||||
{iso(m.data.pubDate).slice(0, 10)}
|
||||
</time>
|
||||
<div class="mom-card">
|
||||
{m.data.image && (
|
||||
<figure
|
||||
class:list={[
|
||||
'mom-fig',
|
||||
(m.data.image.width ?? 0) < (m.data.image.height ?? 0) && 'vt',
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
src={m.data.image}
|
||||
alt={m.data.text ?? m.data.tags.join(' ')}
|
||||
widths={[480, 840]}
|
||||
sizes="(min-width: 760px) 420px, calc(100vw - 140px)"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
{m.data.text && <p>{m.data.text}</p>}
|
||||
{m.data.tags.length > 0 && (
|
||||
<p class="mom-tags">
|
||||
{m.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
font-size: var(--text-h1);
|
||||
line-height: var(--leading-heading);
|
||||
color: var(--ink-1);
|
||||
margin-top: var(--space-5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.page-note {
|
||||
font-size: 13px;
|
||||
color: var(--ink-4);
|
||||
margin-block: var(--space-1) var(--space-4);
|
||||
}
|
||||
.tl {
|
||||
list-style: none;
|
||||
margin: var(--space-4) 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* 时间线骨架:左 mono 日期 + 竖线 + --accent 节点 + 右侧内容卡 */
|
||||
.tl-item {
|
||||
position: relative;
|
||||
margin: 0 0 var(--space-4) calc(var(--space-5) + var(--space-4));
|
||||
padding-left: var(--space-4);
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
.tl-item:last-child { margin-bottom: 0; }
|
||||
.tl-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: 7px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--bg);
|
||||
}
|
||||
.tl-date {
|
||||
position: absolute;
|
||||
left: calc(-1 * (var(--space-5) + var(--space-4)));
|
||||
top: 4px;
|
||||
width: var(--space-5);
|
||||
text-align: right;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-4);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.mom-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
.mom-card > p { font-size: 15px; color: var(--ink-2); }
|
||||
.mom-fig {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0 0 var(--space-2);
|
||||
max-width: 420px;
|
||||
}
|
||||
.mom-fig.vt { max-width: 260px; }
|
||||
.mom-fig :global(img) { width: 100%; height: auto; }
|
||||
.mom-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--ink-3);
|
||||
background: var(--bg);
|
||||
border-radius: 5px;
|
||||
padding: 1px 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 窄屏:日期卡到卡片上方,竖线左移收窄 */
|
||||
@media (max-width: 760px) {
|
||||
.tl-item { margin-left: var(--space-3); }
|
||||
.tl-date {
|
||||
position: static;
|
||||
display: block;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.mom-card { padding: var(--space-2) var(--space-3); }
|
||||
.mom-fig { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
@@ -28,6 +28,42 @@ const dict = {
|
||||
zh: '本文由机器翻译自中文原文,欢迎指正。',
|
||||
en: 'Machine-translated from the Chinese original. Corrections welcome.',
|
||||
},
|
||||
|
||||
// ===== T2 车道(op-gitea):Footer/SearchDialog/gallery/moments/about 静态文案 =====
|
||||
// footer_icp 是法定备案标识,英文页照抄不译
|
||||
search_no_results: { zh: '无结果', en: 'No results' },
|
||||
footer_icp: { zh: '陕ICP备2026014860号-1', en: '陕ICP备2026014860号-1' },
|
||||
gallery_title: { zh: '图库', en: 'Gallery' },
|
||||
gallery_filter_legend: { zh: '分组筛选', en: 'Filter by group' },
|
||||
gallery_filter_all: { zh: '全部', en: 'All' },
|
||||
gallery_filter_street: { zh: '街拍', en: 'Street' },
|
||||
gallery_filter_landscape: { zh: '风景', en: 'Landscape' },
|
||||
// {n}/{title}/{exif} 为占位符,调用点 .replace() 填值
|
||||
gallery_count: { zh: '共 {n} 张 · EXIF 构建期自动提取', en: '{n} photos · EXIF extracted at build time' },
|
||||
gallery_view_aria: { zh: '查看大图:{title}', en: 'View photo: {title}' },
|
||||
gallery_view_aria_exif: { zh: '({exif})', en: ' ({exif})' },
|
||||
gallery_lightbox_label: { zh: '照片查看', en: 'Photo viewer' },
|
||||
gallery_lightbox_close: { zh: '关闭(Esc)', en: 'Close (Esc)' },
|
||||
gallery_lightbox_prev: { zh: '上一张(←)', en: 'Previous (←)' },
|
||||
gallery_lightbox_next: { zh: '下一张(→)', en: 'Next (→)' },
|
||||
gallery_lightbox_original: { zh: '原图', en: 'Original' },
|
||||
gallery_lightbox_download: { zh: '下载', en: 'Download' },
|
||||
moments_title: { zh: '瞬间', en: 'Moments' },
|
||||
moments_note: { zh: '零碎的记录:随手拍、一句话感想、开源动态。', en: 'Fragmented notes: casual shots, one-liners, and open-source updates.' },
|
||||
about_title: { zh: '关于', en: 'About' },
|
||||
about_tracks_title: { zh: '兴趣版图', en: 'Interests' },
|
||||
about_track_reading_name: { zh: '阅读', en: 'Reading' },
|
||||
about_track_reading_desc: { zh: '文学与戏剧为主,读到哪写到哪。', en: 'Mostly literature and drama; I write as I read.' },
|
||||
about_track_tech_name: { zh: '技术', en: 'Tech' },
|
||||
about_track_tech_desc: { zh: 'Linux、容器与自托管,把家里的小机器折腾成顺手的工具。', en: 'Linux, containers, and self-hosting — turning the little machines at home into handy tools.' },
|
||||
about_track_photo_name: { zh: '摄影', en: 'Photography' },
|
||||
about_track_photo_desc: { zh: '街拍与山野,随身一台无反,在等光的间隙按快门。', en: 'Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light.' },
|
||||
about_build_aria: { zh: '本站构建说明', en: 'Site build notes' },
|
||||
about_build_title: { zh: '本站如何构建', en: 'How this site is built' },
|
||||
about_build_desc: {
|
||||
zh: '页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea 网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管 Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。',
|
||||
en: 'Pages are statically generated by Astro with no runtime framework JS. Content lives on a self-hosted gitea — my local editor and the gitea web UI write to the same git repository, and each push is built and published automatically by the homelab. Comments run on self-hosted Artalk. The built-from line in the footer links to the commit behind this build, keeping the process transparent and traceable.',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type UiKey = keyof typeof dict;
|
||||
|
||||
+2
-116
@@ -1,118 +1,4 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import IdCard from '../components/IdCard.astro';
|
||||
import AboutPage from '../components/pages/AboutPage.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="关于 · wait4x">
|
||||
<h1 class="page-title">关于</h1>
|
||||
|
||||
<IdCard />
|
||||
|
||||
<section class="tracks" aria-label="兴趣版图">
|
||||
<h2 class="sec-title">兴趣版图</h2>
|
||||
<ul>
|
||||
<li class="track-row">
|
||||
<span class="track-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 4h6a4 4 0 0 1 4 4v13a3 3 0 0 0-3-3H2z"></path>
|
||||
<path d="M22 4h-6a4 4 0 0 0-4 4v13a3 3 0 0 1 3-3h7z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<strong>阅读</strong>
|
||||
<p>文学与戏剧为主,读到哪写到哪。</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="track-row">
|
||||
<span class="track-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m4 5.5 6.5 6.5L4 18.5"></path>
|
||||
<path d="M12.5 18.5H20"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<strong>技术</strong>
|
||||
<p>Linux、容器与自托管,把家里的小机器折腾成顺手的工具。</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="track-row">
|
||||
<span class="track-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
|
||||
<circle cx="12" cy="13" r="4"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<strong>摄影</strong>
|
||||
<p>街拍与山野,随身一台无反,在等光的间隙按快门。</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="build" aria-label="本站构建说明">
|
||||
<h2 class="sec-title">本站如何构建</h2>
|
||||
<p>
|
||||
页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea
|
||||
网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管
|
||||
Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。
|
||||
</p>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
font-size: var(--text-h1);
|
||||
line-height: var(--leading-heading);
|
||||
color: var(--ink-1);
|
||||
margin-top: var(--space-5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.sec-title {
|
||||
font-size: 14px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--ink-3);
|
||||
letter-spacing: 0.08em;
|
||||
margin: var(--space-5) 0 var(--space-3);
|
||||
}
|
||||
.tracks ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.track-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.track-icon {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-soft);
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.track-row strong {
|
||||
font-size: 15px;
|
||||
color: var(--ink-1);
|
||||
}
|
||||
.track-row p {
|
||||
font-size: 14px;
|
||||
color: var(--ink-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.build p {
|
||||
font-size: 15px;
|
||||
color: var(--ink-2);
|
||||
max-width: var(--measure);
|
||||
}
|
||||
</style>
|
||||
<AboutPage locale="zh" />
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
import AboutPage from '../../components/pages/AboutPage.astro';
|
||||
---
|
||||
<AboutPage locale="en" />
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
import GalleryPage from '../../components/pages/GalleryPage.astro';
|
||||
---
|
||||
<GalleryPage locale="en" />
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
import MomentsPage from '../../components/pages/MomentsPage.astro';
|
||||
---
|
||||
<MomentsPage locale="en" />
|
||||
@@ -1,393 +1,4 @@
|
||||
---
|
||||
import { Image, getImage } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import type { PhotoExif } from '../../lib/exif';
|
||||
|
||||
const photos = await getCollection('photos');
|
||||
|
||||
// EXIF 参数条(机身/镜头/光圈/快门/ISO)构建期拼好整串,灯箱运行时只做文本替换
|
||||
function formatExif(exif: PhotoExif | undefined): string {
|
||||
if (!exif) return '';
|
||||
const shutter =
|
||||
exif.exposureTime === undefined
|
||||
? undefined
|
||||
: exif.exposureTime >= 1
|
||||
? `${exif.exposureTime}s`
|
||||
: `1/${Math.round(1 / exif.exposureTime)}s`;
|
||||
return [
|
||||
[exif.make, exif.model].filter(Boolean).join(' ') || undefined,
|
||||
exif.lens,
|
||||
exif.fNumber === undefined ? undefined : `f/${exif.fNumber}`,
|
||||
shutter,
|
||||
exif.iso === undefined ? undefined : `ISO ${exif.iso}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
/* masonry:行高/列距用容器宽百分比(0.92cqw/1.62cqw,在 864px 版心即 8px/14px 档),
|
||||
行高随断点等比缩放,构建期一份 row-span 在 2/3 列全断点精确成立——
|
||||
px 定值行高会让格高随列宽漂移,窄屏裁切失控 */
|
||||
const ROW = 0.0092;
|
||||
const GAP = 0.0162;
|
||||
const spanFor = (w: number, h: number, cols: number) =>
|
||||
Math.round(((1 - GAP * (cols - 1)) / (cols * (w / h)) + GAP) / (ROW + GAP));
|
||||
|
||||
const cards = await Promise.all(
|
||||
photos.map(async (photo) => {
|
||||
// 宽高比来源:photos loader 产出 ImageMetadata 的 width/height(orientation 已修正)
|
||||
const w = photo.data.image.width ?? 4;
|
||||
const h = photo.data.image.height ?? 3;
|
||||
const full = await getImage({ src: photo.data.image, width: 1440 });
|
||||
return {
|
||||
group: photo.data.group,
|
||||
title: photo.data.title,
|
||||
alt: photo.data.alt ?? photo.data.title,
|
||||
image: photo.data.image,
|
||||
exif: formatExif(photo.data.exif),
|
||||
fullSrc: full.src,
|
||||
// 原图走仓外 store(构建时独立 rsync 到 /photos-originals/),git 不背 196M
|
||||
original: `/photos-originals/${photo.id}.jpg`,
|
||||
w,
|
||||
h,
|
||||
s3: spanFor(w, h, 3),
|
||||
s2: spanFor(w, h, 2),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const first = cards[0];
|
||||
import GalleryPage from '../../components/pages/GalleryPage.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="图库 · wait4x">
|
||||
<h1 class="page-title">图库</h1>
|
||||
<div class="g-main">
|
||||
<fieldset class="filters">
|
||||
<legend class="sr-only">分组筛选</legend>
|
||||
<input class="f-input" type="radio" name="gallery-group" id="f-all" checked />
|
||||
<label class="tap chip" for="f-all">全部</label>
|
||||
<input class="f-input" type="radio" name="gallery-group" id="f-street" />
|
||||
<label class="tap chip" for="f-street">街拍</label>
|
||||
<input class="f-input" type="radio" name="gallery-group" id="f-landscape" />
|
||||
<label class="tap chip" for="f-landscape">风景</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="stage">
|
||||
<ul class="wall">
|
||||
{cards.map((card) => (
|
||||
<li
|
||||
class="shot"
|
||||
data-group={card.group}
|
||||
style={`aspect-ratio:${card.w}/${card.h}; --s3:${card.s3}; --s2:${card.s2}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="shot-btn"
|
||||
data-title={card.title}
|
||||
data-exif={card.exif}
|
||||
data-full={card.fullSrc}
|
||||
data-original={card.original}
|
||||
aria-label={`查看大图:${card.title}${card.exif ? `(${card.exif})` : ''}`}
|
||||
>
|
||||
<Image
|
||||
src={card.image}
|
||||
alt={card.alt}
|
||||
widths={[320, 560, 720]}
|
||||
sizes="(min-width: 820px) 30vw, 46vw"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p class="g-note">共 {cards.length} 张 · EXIF 构建期自动提取</p>
|
||||
</div>
|
||||
|
||||
<dialog id="lb" aria-label="照片查看">
|
||||
<button id="lb-close" class="tap lbnav" type="button" aria-label="关闭(Esc)">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
||||
<path d="m5 5 14 14M19 5 5 19"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="lb-figure">
|
||||
<img id="lb-img" src={first?.fullSrc} alt={first?.alt ?? ''} />
|
||||
</div>
|
||||
<div class="lb-bottom">
|
||||
<button id="lb-prev" class="tap lbnav" type="button" aria-label="上一张(←)">
|
||||
<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="m14.5 5.5-6.5 6.5 6.5 6.5"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="lb-meta">
|
||||
<strong id="lb-title">{first?.title}</strong>
|
||||
<span id="lb-exif">{first?.exif}</span>
|
||||
<span class="lb-orig">
|
||||
<a id="lb-original" href={first?.original} target="_blank" rel="noopener">原图</a>
|
||||
<a id="lb-download" href={first?.original} download>下载</a>
|
||||
</span>
|
||||
</div>
|
||||
<button id="lb-next" class="tap lbnav" type="button" aria-label="下一张(→)">
|
||||
<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>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
// 灯箱:原生 dialog,←→ 切换、Esc 原生关闭;触屏快滑翻页是键盘的等价物。
|
||||
// ClientRouter 下模块脚本只执行一次,接线挂在 astro:page-load 按当前 DOM 重装
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
const lb = document.getElementById('lb') as HTMLDialogElement | null;
|
||||
const shots = [...document.querySelectorAll<HTMLButtonElement>('.shot-btn')];
|
||||
if (!lb || shots.length === 0) return;
|
||||
|
||||
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 reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let idx = 0;
|
||||
|
||||
const show = (i: number) => {
|
||||
idx = (i + shots.length) % shots.length;
|
||||
const btn = shots[idx]!;
|
||||
img.src = btn.dataset.full ?? '';
|
||||
img.alt = btn.ariaLabel ?? '';
|
||||
titleEl.textContent = btn.dataset.title ?? '';
|
||||
exifEl.textContent = btn.dataset.exif ?? '';
|
||||
const orig = btn.dataset.original ?? '';
|
||||
(document.getElementById('lb-original') as HTMLAnchorElement).href = orig;
|
||||
const dl = document.getElementById('lb-download') as HTMLAnchorElement;
|
||||
dl.href = orig;
|
||||
dl.setAttribute('download', btn.dataset.title ?? 'photo');
|
||||
if (!reduced.matches) {
|
||||
img.style.opacity = '0';
|
||||
requestAnimationFrame(() => {
|
||||
img.style.opacity = '1';
|
||||
});
|
||||
}
|
||||
if (!lb.open) lb.showModal();
|
||||
};
|
||||
|
||||
shots.forEach((btn, i) => btn.addEventListener('click', () => show(i)));
|
||||
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('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
show(idx - 1);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
show(idx + 1);
|
||||
}
|
||||
});
|
||||
let x0: number | null = null;
|
||||
lb.addEventListener('touchstart', (e) => {
|
||||
x0 = e.touches[0]!.clientX;
|
||||
}, { passive: true });
|
||||
lb.addEventListener('touchend', (e) => {
|
||||
if (x0 === null) return;
|
||||
const dx = e.changedTouches[0]!.clientX - x0;
|
||||
if (Math.abs(dx) > 40) show(idx + (dx < 0 ? 1 : -1));
|
||||
x0 = null;
|
||||
}, { passive: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 版心放行:图库加宽到 measure+176(丙路线 §3;BaseLayout 默认 720) */
|
||||
:global(main#main:has(.g-main)) {
|
||||
max-width: calc(var(--measure) + 176px);
|
||||
}
|
||||
.page-title {
|
||||
font-size: var(--text-h1);
|
||||
line-height: var(--leading-heading);
|
||||
color: var(--ink-1);
|
||||
margin-top: var(--space-5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* 分组筛选:纯 CSS :checked + :has,radio 原生键盘可达(方向键切换) */
|
||||
.filters {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: var(--space-3) 0 var(--space-4);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.sr-only,
|
||||
.f-input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip {
|
||||
padding-inline: 13px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
color: var(--ink-3);
|
||||
cursor: pointer;
|
||||
transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||
}
|
||||
.chip:hover { color: var(--ink-1); border-color: var(--ink-4); }
|
||||
.f-input:focus-visible + .chip { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.f-input:checked + .chip { color: var(--ink-1); border-color: var(--ink-3); font-weight: 500; }
|
||||
.g-main:has(#f-street:checked) .shot:not([data-group='street']) { display: none; }
|
||||
.g-main:has(#f-landscape:checked) .shot:not([data-group='landscape']) { display: none; }
|
||||
|
||||
/* masonry:stage 是容器查询锚,行高/列距以 stage 宽为基(cqw) */
|
||||
.stage { container-type: inline-size; }
|
||||
.wall {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-auto-rows: 0.92cqw; /* 8px 档 @ 864px 版心 */
|
||||
grid-auto-flow: dense;
|
||||
gap: 1.62cqw; /* 14px 档 @ 864px 版心 */
|
||||
}
|
||||
@media (min-width: 820px) {
|
||||
.wall { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
.shot {
|
||||
grid-row-end: span var(--s2, 10);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (min-width: 820px) {
|
||||
.shot { grid-row-end: span var(--s3, 12); }
|
||||
}
|
||||
.shot-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.shot-btn :global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.g-note {
|
||||
margin-top: var(--space-3);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
/* 灯箱:全屏原生 dialog,高度用 dvh(规避 100vh 移动端地址栏陷阱) */
|
||||
#lb {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
#lb::backdrop { background: rgba(8, 10, 14, 0.95); }
|
||||
.lb-figure {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-5) var(--space-4) var(--space-2);
|
||||
}
|
||||
#lb-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
transition: opacity 160ms var(--ease);
|
||||
}
|
||||
.lb-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
color: var(--code-ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.lb-orig {
|
||||
display: inline-flex;
|
||||
gap: var(--space-3);
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
.lb-orig a {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
.lb-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.lb-meta strong {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
}
|
||||
.lb-meta span {
|
||||
opacity: 0.82;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lbnav {
|
||||
flex: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #e6e9ee;
|
||||
}
|
||||
.lbnav:hover { background: rgba(255, 255, 255, 0.12); }
|
||||
#lb-close {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.lb-bottom { flex-wrap: wrap; }
|
||||
.lb-orig {
|
||||
display: inline-flex;
|
||||
gap: var(--space-3);
|
||||
margin-left: var(--space-2);
|
||||
}
|
||||
.lb-orig a {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
.lb-meta { order: 3; flex-basis: 100%; }
|
||||
}
|
||||
</style>
|
||||
<GalleryPage locale="zh" />
|
||||
@@ -1,142 +1,4 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const moments = (await getCollection('moments')).sort(
|
||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||
);
|
||||
const iso = (d: Date) => d.toISOString();
|
||||
import MomentsPage from '../../components/pages/MomentsPage.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="瞬间 · wait4x">
|
||||
<h1 class="page-title">瞬间</h1>
|
||||
<p class="page-note">零碎的记录:随手拍、一句话感想、开源动态。</p>
|
||||
<ol class="tl">
|
||||
{moments.map((m) => (
|
||||
<li class="tl-item">
|
||||
<time class="tl-date" datetime={iso(m.data.pubDate)}>
|
||||
{iso(m.data.pubDate).slice(0, 10)}
|
||||
</time>
|
||||
<div class="mom-card">
|
||||
{m.data.image && (
|
||||
<figure
|
||||
class:list={[
|
||||
'mom-fig',
|
||||
(m.data.image.width ?? 0) < (m.data.image.height ?? 0) && 'vt',
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
src={m.data.image}
|
||||
alt={m.data.text ?? m.data.tags.join(' ')}
|
||||
widths={[480, 840]}
|
||||
sizes="(min-width: 760px) 420px, calc(100vw - 140px)"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
{m.data.text && <p>{m.data.text}</p>}
|
||||
{m.data.tags.length > 0 && (
|
||||
<p class="mom-tags">
|
||||
{m.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
font-size: var(--text-h1);
|
||||
line-height: var(--leading-heading);
|
||||
color: var(--ink-1);
|
||||
margin-top: var(--space-5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
.page-note {
|
||||
font-size: 13px;
|
||||
color: var(--ink-4);
|
||||
margin-block: var(--space-1) var(--space-4);
|
||||
}
|
||||
.tl {
|
||||
list-style: none;
|
||||
margin: var(--space-4) 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* 时间线骨架:左 mono 日期 + 竖线 + --accent 节点 + 右侧内容卡 */
|
||||
.tl-item {
|
||||
position: relative;
|
||||
margin: 0 0 var(--space-4) calc(var(--space-5) + var(--space-4));
|
||||
padding-left: var(--space-4);
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
.tl-item:last-child { margin-bottom: 0; }
|
||||
.tl-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: 7px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--bg);
|
||||
}
|
||||
.tl-date {
|
||||
position: absolute;
|
||||
left: calc(-1 * (var(--space-5) + var(--space-4)));
|
||||
top: 4px;
|
||||
width: var(--space-5);
|
||||
text-align: right;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-4);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.mom-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
.mom-card > p { font-size: 15px; color: var(--ink-2); }
|
||||
.mom-fig {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 0 0 var(--space-2);
|
||||
max-width: 420px;
|
||||
}
|
||||
.mom-fig.vt { max-width: 260px; }
|
||||
.mom-fig :global(img) { width: 100%; height: auto; }
|
||||
.mom-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--ink-3);
|
||||
background: var(--bg);
|
||||
border-radius: 5px;
|
||||
padding: 1px 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 窄屏:日期卡到卡片上方,竖线左移收窄 */
|
||||
@media (max-width: 760px) {
|
||||
.tl-item { margin-left: var(--space-3); }
|
||||
.tl-date {
|
||||
position: static;
|
||||
display: block;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.mom-card { padding: var(--space-2) var(--space-3); }
|
||||
.mom-fig { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
<MomentsPage locale="zh" />
|
||||
Reference in new issue
Block a user