feat: 重构博客为水墨纸质风格 + 搭建后台管理系统
- 重新设计全站 UI:parchment/ink/terracotta 水墨纸质色系,宋式 serif 排版 - 新增页面:文章列表、文章详情、分类、标签、关于 - GSAP ScrollTrigger 滚动动画 + 逐字揭示效果 - 后台管理系统 /admin:文章/分类/标签 CRUD,JSON 文件存储 - 登录认证(cookie session) - 设计系统文档 UI.md
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
"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" });
|
||||
}
|
||||
|
||||
function FeaturedCard({ post }: { post: Post }) {
|
||||
return (
|
||||
<Link href={`/posts/${post.slug}`} className="group block featured-card">
|
||||
<article className="relative p-7 md:p-10 rounded-2xl bg-cream border border-warm-gray/10 hover:border-terracotta/20 hover:shadow-lg hover:shadow-terracotta/5 transition-all duration-500">
|
||||
<span className="inline-block font-sans text-sm tracking-widest text-terracotta uppercase mb-4">
|
||||
{post.category}
|
||||
</span>
|
||||
<h3 className="font-display text-2xl md:text-3xl font-medium text-ink leading-snug group-hover:text-terracotta transition-colors duration-300 mb-4">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="font-body text-base text-ink-muted leading-relaxed line-clamp-2 mb-5">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 font-sans text-sm text-ink-muted">
|
||||
<time>{formatDate(post.date)}</time>
|
||||
<span className="w-1 h-1 rounded-full bg-warm-gray" />
|
||||
<span>{post.readingTime} min read</span>
|
||||
</div>
|
||||
<div className="absolute top-7 right-7 md:top-10 md:right-10 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-terracotta">
|
||||
<path d="M5 10h10M11 6l4 4-4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeaturedGrid({ posts }: { posts: Post[] }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.from(".featured-card", {
|
||||
y: 50,
|
||||
opacity: 0,
|
||||
duration: 0.8,
|
||||
stagger: 0.15,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: ref.current,
|
||||
start: "top 85%",
|
||||
},
|
||||
});
|
||||
}, ref.current);
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={ref} className="px-page max-w-5xl mx-auto pb-16">
|
||||
<div className="grid md:grid-cols-2 gap-5">
|
||||
{posts.map((post) => (
|
||||
<FeaturedCard key={post.slug} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function RecentList({ posts }: { posts: Post[] }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.from(".recent-item", {
|
||||
y: 30,
|
||||
opacity: 0,
|
||||
duration: 0.6,
|
||||
stagger: 0.08,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: ref.current,
|
||||
start: "top 85%",
|
||||
},
|
||||
});
|
||||
}, ref.current);
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={ref} className="px-page max-w-5xl mx-auto pb-24">
|
||||
<div className="divider-ornament mb-10">
|
||||
<span className="font-display text-sm italic whitespace-nowrap">最新文章</span>
|
||||
</div>
|
||||
<div className="space-y-0">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/posts/${post.slug}`}
|
||||
className="recent-item group block"
|
||||
>
|
||||
<article className="flex items-baseline gap-6 py-7 border-b border-warm-gray/10 hover:bg-cream -mx-4 px-4 rounded-lg transition-all duration-300">
|
||||
<time className="shrink-0 font-sans text-sm text-ink-muted tabular-nums w-28 pt-0.5">
|
||||
{formatDate(post.date)}
|
||||
</time>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-display text-xl font-medium text-ink group-hover:text-terracotta transition-colors duration-300 truncate">
|
||||
{post.title}
|
||||
</h3>
|
||||
<p className="mt-1.5 font-body text-base text-ink-muted line-clamp-1">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
<span className="hidden sm:inline-block shrink-0 font-sans text-sm text-ink-muted tracking-wide">
|
||||
{post.category}
|
||||
</span>
|
||||
</article>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-10 text-center">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center gap-2 font-sans text-sm text-ink-muted hover:text-terracotta transition-colors duration-300"
|
||||
>
|
||||
查看全部文章
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M3 7h8M8 4l3 3-3 3" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user