初始提交:时之刃的小站

基于 VermilionVoid(朱墨留白)主题的个人博客。
- 站点配置集中在 src/data/site.ts 与 src/data/profile.ts
- 30 篇文章、8 条偶得、相册
- 评论使用自托管 twikoo
- 部署走 deploy.sh(rsync 到树莓派 + 云服务器)
This commit is contained in:
dsh
2026-09-14 20:44:12 +08:00
commit 6f4cc13a20
209 changed files with 36176 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro"
import SiteHeader from "@/components/site-header.astro"
import { Footer } from "@/components/footer"
import { siteTitleSuffix } from "@/data/site"
import { getAllAlbumTags, getAlbums } from "@/lib/gallery"
const title = `相册 | ${siteTitleSuffix}`
const description = "按相册整理的照片"
const albums = getAlbums()
const allTags = getAllAlbumTags(albums)
---
<BaseLayout {title} {description} fontProfile="interface">
<div class="min-h-screen bg-background dark:bg-surface-subtle">
<SiteHeader />
<main class="pt-24 sm:pt-32 pb-16">
<div class="max-w-6xl mx-auto px-6">
{/* 页头 */}
<section class="mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-foreground mb-3 flex items-center gap-3">
<span class="w-1 h-8 bg-primary rounded-full" />
相册
</h1>
<p class="text-base sm:text-lg text-muted-foreground">
共 {albums.length} 个相册,{albums.reduce((n, a) => n + a.photoCount, 0)} 张照片。
</p>
</section>
{/* 标签筛选 */}
{
allTags.length > 0 && (
<div data-album-filter class="mb-8 flex flex-wrap gap-2 border-b border-border pb-5">
<button
type="button"
data-tag="all"
class="rounded-full border border-foreground bg-foreground px-3.5 py-1.5 text-[13px] text-background transition-all"
>
全部
</button>
{allTags.map((tag) => (
<button
type="button"
data-tag={tag}
class="rounded-full border border-border px-3.5 py-1.5 text-[13px] text-muted-foreground transition-all hover:text-foreground"
>
{tag}
</button>
))}
</div>
)
}
{/* 相册网格 */}
{
albums.length > 0 ? (
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{albums.map((album) => (
<a
href={`/gallery/${album.id}/`}
data-album-card
data-tags={(album.tags ?? []).join(",")}
class="group relative block overflow-hidden rounded-xl border border-border/50 bg-card transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/40"
>
<div class="relative aspect-4/3 overflow-hidden bg-secondary">
{album.cover ? (
<img
src={album.cover}
alt={album.name}
loading="lazy"
decoding="async"
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div class="flex h-full w-full items-center justify-center text-4xl text-muted-foreground/40">
📷
</div>
)}
<div class="absolute inset-0 bg-gradient-to-t from-black/75 via-black/25 to-transparent" />
<div class="absolute right-2 top-2 rounded-full bg-black/50 px-2 py-1 text-xs font-medium text-white backdrop-blur-sm">
{album.photoCount} 张
</div>
<div class="absolute inset-x-0 bottom-0 p-4">
<h3 class="line-clamp-1 text-base font-bold text-white drop-shadow-lg">
{album.name}
</h3>
{album.description && (
<p class="mt-1 line-clamp-1 text-xs leading-relaxed text-white/75">
{album.description}
</p>
)}
<div class="mt-1.5 flex flex-wrap items-center gap-3 text-xs text-white/70">
{album.date && <span>{album.date}</span>}
{album.location && <span>📍 {album.location}</span>}
</div>
{album.tags && album.tags.length > 0 && (
<div class="mt-2 flex flex-wrap gap-1">
{album.tags.slice(0, 4).map((tag) => (
<span class="rounded bg-white/20 px-1.5 py-0.5 text-[0.6rem] text-white/90 backdrop-blur-sm">
{tag}
</span>
))}
</div>
)}
</div>
</div>
</a>
))}
</div>
) : (
<div class="py-20 text-center text-muted-foreground">还没有相册</div>
)
}
</div>
</main>
<Footer />
</div>
</BaseLayout>
<script>
// 标签筛选。ClientRouter 切页后需要重新绑定,所以监听 astro:page-load。
const initAlbumFilter = () => {
const filter = document.querySelector("[data-album-filter]")
if (!filter || filter.dataset.bound === "true") return
filter.dataset.bound = "true"
const activeClass = ["border-foreground", "bg-foreground", "text-background"]
const idleClass = ["border-border", "text-muted-foreground"]
filter.addEventListener("click", (event) => {
const button = (event.target as Element | null)?.closest("button[data-tag]")
if (!(button instanceof HTMLElement)) return
const selected = button.dataset.tag ?? "all"
for (const item of filter.querySelectorAll<HTMLElement>("button[data-tag]")) {
const on = item === button
item.classList.remove(...activeClass, ...idleClass)
item.classList.add(...(on ? activeClass : idleClass))
}
for (const card of document.querySelectorAll<HTMLElement>("[data-album-card]")) {
const tags = (card.dataset.tags ?? "").split(",").filter(Boolean)
card.style.display = selected === "all" || tags.includes(selected) ? "" : "none"
}
})
}
initAlbumFilter()
document.addEventListener("astro:page-load", initAlbumFilter)
</script>