Files
NewBlog/src/pages/gallery/[album].astro
T
dsh 0cfcbd060e 页脚改版:删掉重复的导航栏,桌面补站点信息;我的介绍可点去关于页
页脚「导航」栏
- 它是顶部导航的逐项复制(9 条名字和目标完全一样),手机上白占 312px
- 已整栏删除;桌面端改用「最新文章」(最近 4 篇,按发布时间倒序,
  不把置顶提前),把重复变成发现
- 手机端「最新文章」隐藏(上面就是文章列表,再列一遍没意义)
- 不影响 SEO:顶部导航 hidden md:flex 和移动端菜单里的同一批链接
  在每页 HTML 里都还在,爬虫照样抓得到

站点信息(新增)
- 新增 src/lib/site-stats.ts:文章数/偶得数/分类数/标签数/总字数
  在构建期算;「已运行 N 天」「最近更新」由页脚内联脚本按真实时间刷新,
  避免站点久不重建时数字停在构建那天;服务端先渲染构建时兜底值,关 JS 不空
- 页脚加一条站点信息带:建站时间 / 已运行 / 文章 / 偶得 / 总字数 / 最近更新
  桌面 6 列一行,手机 3 列两行
- 实测数值:2026-07-17 建站、已运行 60 天、30 篇文章、8 条偶得、
  2.9 万字、最近更新 59 天前

「我的介绍」跳关于页
- 页脚 BRAND 栏整块包 <a href="/about/">,加 hover 反馈(头像描边转主色、
  站名转主色、右上箭头位移)和常显的「关于我 →」提示(手机没有 hover,
  不能只在 hover 才出现)
- 侧栏作者卡片同样处理。注意这里**只包到签名那一段** —— 下面的社交图标
  本身是 <a>,<a> 不能嵌套 <a>;页脚 BRAND 栏没有嵌套链接,所以可以整块包
- 两处都给 aria-label,避免读屏把「头像 alt + 站名 + 签名」念成链接名

工程改动
- footer.tsx -> footer.astro:React 组件不能在构建期 await 统计数据,
  换成 Astro 组件后可以直接取;12 处引用同步改成默认导入
- README 补「页脚」一节,并修正组件路径

验证(headless Chrome 131,禁用缓存)
- 桌面 1280:页脚 529px,三栏(我的介绍/最新文章/联系)+ 站点信息 + 底栏,
  页脚内 nav 链接数 0,无横向溢出
- 手机 390:页脚 822px(原 957),最新文章 display:none,三栏只剩两栏
- 点页脚「我的介绍」-> /about/;点侧栏作者卡片 -> /about/,均正常
- 页面内 `a a` 嵌套数量:页脚 0、侧栏 0
2026-09-15 09:55:26 +08:00

237 lines
9.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import BaseLayout from "@/layouts/BaseLayout.astro"
import SiteHeader from "@/components/site-header.astro"
import Footer from "@/components/footer.astro"
import { siteTitleSuffix } from "@/data/site"
import { galleryConfig } from "@/data/gallery"
import { getAlbumCover, scanAlbumPhotos } from "@/lib/gallery"
export function getStaticPaths() {
return galleryConfig.albums.map((album) => ({ params: { album: album.id } }))
}
const albumId = Astro.params.album as string
const album = galleryConfig.albums.find((a) => a.id === albumId)
if (!album) {
return Astro.redirect("/404/")
}
const photos = scanAlbumPhotos(album.id)
const cover = getAlbumCover(album, photos)
const columnWidth = galleryConfig.columnWidth || 240
const title = `${album.name} | ${siteTitleSuffix}`
const description = album.description ?? "相册"
---
<BaseLayout {title} {description} fontProfile="interface">
<div class="min-h-screen bg-background dark:bg-surface-subtle">
<SiteHeader />
<main class="pt-20 pb-16">
{/* 封面横幅 */}
<section class="relative w-full overflow-hidden">
{cover ? (
<div class="relative h-[240px] w-full sm:h-[320px]">
<img src={cover} alt={album.name} class="h-full w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/35 to-black/10" />
<a
href="/gallery/"
class="absolute left-4 top-4 rounded-lg bg-black/35 px-3 py-1.5 text-sm text-white/90 backdrop-blur-sm transition-colors hover:bg-black/55 hover:text-white"
>
← 返回相册
</a>
<div class="absolute inset-x-0 bottom-0 p-6">
<div class="mx-auto max-w-6xl">
<h1 class="mb-2 text-2xl font-bold text-white drop-shadow-lg sm:text-3xl">
{album.name}
</h1>
{album.description && (
<p class="mb-2 max-w-2xl line-clamp-2 text-sm leading-relaxed text-white/75">
{album.description}
</p>
)}
<div class="flex flex-wrap items-center gap-4 text-sm text-white/80">
{album.date && <span>🗓 {album.date}</span>}
{album.location && <span>📍 {album.location}</span>}
<span>🖼 {photos.length} 张</span>
</div>
{album.tags && album.tags.length > 0 && (
<div class="mt-2.5 flex flex-wrap gap-1.5">
{album.tags.map((tag) => (
<span class="rounded bg-white/20 px-2 py-0.5 text-xs text-white/90 backdrop-blur-sm">
{tag}
</span>
))}
</div>
)}
</div>
</div>
</div>
) : (
<div class="mx-auto max-w-6xl px-6 pt-4">
<a href="/gallery/" class="mb-3 inline-block text-sm text-primary hover:underline">
← 返回相册
</a>
<h1 class="text-2xl font-bold text-foreground sm:text-3xl">{album.name}</h1>
</div>
)}
</section>
{/* 瀑布流 */}
<section class="mx-auto max-w-6xl px-6 pt-8">
{photos.length > 0 ? (
<div class="gallery-masonry" style={`--col-width: ${columnWidth}px;`}>
{photos.map((photo, index) => (
<button
type="button"
data-lightbox-src={photo}
data-lightbox-index={index}
aria-label={`查看第 ${index + 1} 张照片`}
class="gallery-photo mb-3 block w-full cursor-zoom-in overflow-hidden rounded-xl border border-border/50 bg-card p-0"
>
<img
src={photo}
alt=""
loading="lazy"
decoding="async"
class="h-auto w-full object-cover transition-transform duration-300 hover:scale-[1.03]"
/>
</button>
))}
</div>
) : (
<div class="py-20 text-center text-muted-foreground">
这个相册还没有照片。往 <code class="rounded bg-secondary px-1.5 py-0.5">public/gallery/{album.id}/</code> 里放图片即可。
</div>
)}
</section>
</main>
<Footer />
</div>
</BaseLayout>
<style is:global>
.gallery-masonry {
column-width: var(--col-width, 240px);
column-gap: 0.75rem;
}
.gallery-masonry .gallery-photo {
break-inside: avoid;
}
</style>
<script>
/**
* 无依赖灯箱。
* 用事件委托 + 每次打开时重新收集当前页的照片,因此 ClientRouter 切页后依然有效。
*/
const LB_ID = "vv-lightbox"
const ensureOverlay = (): HTMLElement => {
let overlay = document.getElementById(LB_ID)
if (overlay) return overlay
overlay = document.createElement("div")
overlay.id = LB_ID
overlay.setAttribute("role", "dialog")
overlay.setAttribute("aria-modal", "true")
overlay.className =
"fixed inset-0 z-[100] hidden items-center justify-center bg-black/90 backdrop-blur-sm"
overlay.innerHTML = `
<button type="button" data-lb-close aria-label="关闭"
class="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20">×</button>
<button type="button" data-lb-prev aria-label="上一张"
class="absolute left-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:left-6">‹</button>
<button type="button" data-lb-next aria-label="下一张"
class="absolute right-3 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-white/10 text-2xl leading-none text-white/90 transition-colors hover:bg-white/20 sm:right-6">›</button>
<img data-lb-img src="" alt="" class="max-h-[88vh] max-w-[92vw] object-contain" />
<div data-lb-count
class="absolute bottom-5 left-1/2 -translate-x-1/2 rounded-full bg-white/10 px-3 py-1 font-mono text-xs text-white/80"></div>
`
document.body.appendChild(overlay)
return overlay
}
let sources: string[] = []
let current = 0
const render = (overlay: HTMLElement) => {
const img = overlay.querySelector<HTMLImageElement>("[data-lb-img]")
const counter = overlay.querySelector<HTMLElement>("[data-lb-count]")
if (img) img.src = sources[current] ?? ""
if (counter) counter.textContent = `${current + 1} / ${sources.length}`
const single = sources.length < 2
for (const sel of ["[data-lb-prev]", "[data-lb-next]"]) {
const el = overlay.querySelector<HTMLElement>(sel)
if (el) el.style.display = single ? "none" : ""
}
}
const openAt = (index: number) => {
const overlay = ensureOverlay()
current = index
render(overlay)
overlay.classList.remove("hidden")
overlay.classList.add("flex")
document.body.style.overflow = "hidden"
}
const close = () => {
const overlay = document.getElementById(LB_ID)
if (!overlay) return
overlay.classList.add("hidden")
overlay.classList.remove("flex")
document.body.style.overflow = ""
}
const step = (delta: number) => {
if (sources.length < 2) return
current = (current + delta + sources.length) % sources.length
const overlay = document.getElementById(LB_ID)
if (overlay) render(overlay)
}
if (!(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound) {
;(window as unknown as { __vvLightboxBound?: boolean }).__vvLightboxBound = true
document.addEventListener("click", (event) => {
const target = event.target as Element | null
const trigger = target?.closest<HTMLElement>("[data-lightbox-src]")
if (trigger) {
// 每次打开都重新收集当前页的照片
const all = Array.from(document.querySelectorAll<HTMLElement>("[data-lightbox-src]"))
sources = all.map((el) => el.dataset.lightboxSrc ?? "")
openAt(Number(trigger.dataset.lightboxIndex ?? all.indexOf(trigger)))
return
}
if (target?.closest("[data-lb-close]")) return close()
if (target?.closest("[data-lb-prev]")) return step(-1)
if (target?.closest("[data-lb-next]")) return step(1)
// 点背景关闭(点图片本身不关)
const overlay = document.getElementById(LB_ID)
if (overlay && target === overlay) close()
})
document.addEventListener("keydown", (event) => {
const overlay = document.getElementById(LB_ID)
if (!overlay || overlay.classList.contains("hidden")) return
if (event.key === "Escape") close()
if (event.key === "ArrowLeft") step(-1)
if (event.key === "ArrowRight") step(1)
})
// 切页时收拾干净,避免残留遮罩锁住滚动
document.addEventListener("astro:page-load", () => {
const overlay = document.getElementById(LB_ID)
if (overlay) overlay.remove()
document.body.style.overflow = ""
})
}
</script>