初始提交:时之刃的小站
基于 VermilionVoid(朱墨留白)主题的个人博客。 - 站点配置集中在 src/data/site.ts 与 src/data/profile.ts - 30 篇文章、8 条偶得、相册 - 评论使用自托管 twikoo - 部署走 deploy.sh(rsync 到树莓派 + 云服务器)
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user