feat: 重构博客为水墨纸质风格 + 搭建后台管理系统
- 重新设计全站 UI:parchment/ink/terracotta 水墨纸质色系,宋式 serif 排版 - 新增页面:文章列表、文章详情、分类、标签、关于 - GSAP ScrollTrigger 滚动动画 + 逐字揭示效果 - 后台管理系统 /admin:文章/分类/标签 CRUD,JSON 文件存储 - 登录认证(cookie session) - 设计系统文档 UI.md
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import type { Post, Category, Tag } from "@/lib/store";
|
||||
|
||||
export default function EditPostPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const [post, setPost] = useState<Post | null>(null);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [allTags, setAllTags] = useState<Tag[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
title: "", slug: "", excerpt: "", content: "",
|
||||
category: "", tags: [] as string[], readingTime: 5,
|
||||
featured: false, status: "draft" as "draft" | "published",
|
||||
date: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch(`/api/posts/${id}`).then((r) => r.json()),
|
||||
fetch("/api/categories").then((r) => r.json()),
|
||||
fetch("/api/tags").then((r) => r.json()),
|
||||
]).then(([p, c, t]) => {
|
||||
setPost(p);
|
||||
setCategories(c);
|
||||
setAllTags(t);
|
||||
setForm({
|
||||
title: p.title, slug: p.slug, excerpt: p.excerpt,
|
||||
content: p.content, category: p.category, tags: p.tags,
|
||||
readingTime: p.readingTime, featured: p.featured,
|
||||
status: p.status, date: p.date,
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const res = await fetch(`/api/posts/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
if (res.ok) router.push("/admin/posts");
|
||||
}
|
||||
|
||||
function toggleTag(tagName: string) {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
tags: prev.tags.includes(tagName)
|
||||
? prev.tags.filter((t) => t !== tagName)
|
||||
: [...prev.tags, tagName],
|
||||
}));
|
||||
}
|
||||
|
||||
if (loading) return <div className="font-sans text-ink-muted">加载中...</div>;
|
||||
if (!post) return <div className="font-sans text-red-600">文章未找到</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<button onClick={() => router.back()} className="font-sans text-sm text-ink-muted hover:text-ink transition-colors">← 返回</button>
|
||||
<h1 className="font-display text-3xl font-medium text-ink">编辑文章</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">标题</label>
|
||||
<input
|
||||
value={form.title}
|
||||
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
||||
className="w-full px-4 py-3 rounded-xl border border-warm-gray/20 bg-cream font-display text-lg text-ink focus:outline-none focus:border-terracotta/40 transition-colors"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">Slug</label>
|
||||
<input
|
||||
value={form.slug}
|
||||
onChange={(e) => setForm({ ...form, slug: e.target.value })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">日期</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.date}
|
||||
onChange={(e) => setForm({ ...form, date: e.target.value })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">摘要</label>
|
||||
<textarea
|
||||
value={form.excerpt}
|
||||
onChange={(e) => setForm({ ...form, excerpt: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full px-4 py-3 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">内容(HTML)</label>
|
||||
<textarea
|
||||
value={form.content}
|
||||
onChange={(e) => setForm({ ...form, content: e.target.value })}
|
||||
rows={12}
|
||||
className="w-full px-4 py-3 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors resize-y font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">分类</label>
|
||||
<select
|
||||
value={form.category}
|
||||
onChange={(e) => setForm({ ...form, category: e.target.value })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors"
|
||||
required
|
||||
>
|
||||
<option value="">选择分类</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c.id} value={c.name}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">阅读时间(分钟)</label>
|
||||
<input
|
||||
type="number" min={1}
|
||||
value={form.readingTime}
|
||||
onChange={(e) => setForm({ ...form, readingTime: Number(e.target.value) })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-sans text-sm text-ink-muted mb-1.5">标签</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allTags.map((tag) => (
|
||||
<button key={tag.id} type="button" onClick={() => toggleTag(tag.name)}
|
||||
className={`font-sans text-xs px-3 py-1.5 rounded-full border transition-colors ${
|
||||
form.tags.includes(tag.name)
|
||||
? "bg-terracotta/10 border-terracotta/30 text-terracotta"
|
||||
: "border-warm-gray/20 text-ink-muted hover:border-terracotta/20"
|
||||
}`}
|
||||
>{tag.name}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<label className="flex items-center gap-2 font-sans text-sm text-ink cursor-pointer">
|
||||
<input type="checkbox" checked={form.featured} onChange={(e) => setForm({ ...form, featured: e.target.checked })} className="accent-terracotta" />
|
||||
精选文章
|
||||
</label>
|
||||
<label className="flex items-center gap-2 font-sans text-sm text-ink cursor-pointer">
|
||||
<input type="radio" name="status" checked={form.status === "draft"} onChange={() => setForm({ ...form, status: "draft" })} className="accent-terracotta" />
|
||||
草稿
|
||||
</label>
|
||||
<label className="flex items-center gap-2 font-sans text-sm text-ink cursor-pointer">
|
||||
<input type="radio" name="status" checked={form.status === "published"} onChange={() => setForm({ ...form, status: "published" })} className="accent-terracotta" />
|
||||
发布
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="submit" className="px-6 py-2.5 rounded-lg bg-ink text-cream font-sans text-sm hover:bg-terracotta transition-colors">保存修改</button>
|
||||
<button type="button" onClick={() => router.back()} className="px-6 py-2.5 rounded-lg border border-warm-gray/20 font-sans text-sm text-ink-muted hover:text-ink transition-colors">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user