dce8fe62ea
- 重新设计全站 UI:parchment/ink/terracotta 水墨纸质色系,宋式 serif 排版 - 新增页面:文章列表、文章详情、分类、标签、关于 - GSAP ScrollTrigger 滚动动画 + 逐字揭示效果 - 后台管理系统 /admin:文章/分类/标签 CRUD,JSON 文件存储 - 登录认证(cookie session) - 设计系统文档 UI.md
188 lines
8.0 KiB
TypeScript
188 lines
8.0 KiB
TypeScript
"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>
|
||
);
|
||
}
|