feat: 重构博客为水墨纸质风格 + 搭建后台管理系统

- 重新设计全站 UI:parchment/ink/terracotta 水墨纸质色系,宋式 serif 排版
- 新增页面:文章列表、文章详情、分类、标签、关于
- GSAP ScrollTrigger 滚动动画 + 逐字揭示效果
- 后台管理系统 /admin:文章/分类/标签 CRUD,JSON 文件存储
- 登录认证(cookie session)
- 设计系统文档 UI.md
This commit is contained in:
胡旭
2026-06-24 08:45:28 +08:00
parent 507f12e501
commit dce8fe62ea
35 changed files with 3124 additions and 96 deletions
+128
View File
@@ -0,0 +1,128 @@
"use client";
import Link from "next/link";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import type { Post } from "@/data/posts";
gsap.registerPlugin(ScrollTrigger);
function formatDate(dateStr: string) {
const d = new Date(dateStr);
return d.toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" });
}
export default function BlogList({ posts }: { posts: Post[] }) {
const headerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctxs: gsap.Context[] = [];
// Header animation
if (headerRef.current) {
const ctx = gsap.context(() => {
gsap.from(".blog-header-el", {
y: 30,
opacity: 0,
duration: 0.7,
stagger: 0.1,
ease: "power3.out",
});
}, headerRef.current);
ctxs.push(ctx);
}
// List items stagger on scroll
if (listRef.current) {
const ctx = gsap.context(() => {
gsap.from(".blog-list-item", {
y: 40,
opacity: 0,
duration: 0.7,
stagger: 0.1,
ease: "power3.out",
scrollTrigger: {
trigger: listRef.current,
start: "top 88%",
},
});
}, listRef.current);
ctxs.push(ctx);
}
return () => ctxs.forEach((c) => c.revert());
}, []);
return (
<div className="px-page max-w-5xl mx-auto pt-16 pb-24">
{/* Header */}
<div ref={headerRef} className="mb-14">
<h1 className="blog-header-el font-display text-4xl md:text-5xl font-light text-ink tracking-tight">
</h1>
<p className="blog-header-el mt-3 font-body text-ink-muted max-w-md">
{" "}
<Link href="/categories" className="text-terracotta hover:underline underline-offset-2">
</Link>{" "}
{" "}
<Link href="/tags" className="text-terracotta hover:underline underline-offset-2">
</Link>{" "}
</p>
</div>
{/* Post list */}
<div ref={listRef} className="space-y-0">
{posts.map((post) => (
<Link
key={post.slug}
href={`/posts/${post.slug}`}
className="blog-list-item group block"
>
<article className="relative py-8 border-b border-warm-gray/10 hover:bg-cream -mx-6 px-6 rounded-xl transition-all duration-400">
<div className="flex flex-col md:flex-row md:items-start gap-3 md:gap-8">
<div className="shrink-0 md:w-36 md:pt-1">
<time className="font-sans text-sm text-ink-muted tabular-nums">
{formatDate(post.date)}
</time>
<div className="mt-1 flex items-center gap-2 md:flex-col md:items-start md:gap-1">
<span className="font-sans text-sm text-terracotta">
{post.category}
</span>
<span className="hidden md:block font-sans text-sm text-ink-muted">
{post.readingTime} min
</span>
</div>
</div>
<div className="flex-1 min-w-0">
<h2 className="font-display text-xl md:text-2xl font-medium text-ink group-hover:text-terracotta transition-colors duration-300 leading-snug">
{post.title}
</h2>
<p className="mt-2 font-body text-base text-ink-muted leading-relaxed line-clamp-2 max-w-xl">
{post.excerpt}
</p>
<div className="mt-3 flex flex-wrap gap-2">
{post.tags.slice(0, 4).map((tag) => (
<span key={tag} className="font-sans text-xs px-2.5 py-0.5 rounded-full bg-parchment-deep text-ink-muted">
{tag}
</span>
))}
</div>
</div>
<div className="hidden md:flex shrink-0 items-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 pt-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" className="text-terracotta">
<path d="M7 12h10M13 8l4 4-4 4" />
</svg>
</div>
</div>
</article>
</Link>
))}
</div>
</div>
);
}