Files
sui_blog/src/app/admin/posts/[id]/page.tsx
T
胡旭 dce8fe62ea feat: 重构博客为水墨纸质风格 + 搭建后台管理系统
- 重新设计全站 UI:parchment/ink/terracotta 水墨纸质色系,宋式 serif 排版
- 新增页面:文章列表、文章详情、分类、标签、关于
- GSAP ScrollTrigger 滚动动画 + 逐字揭示效果
- 后台管理系统 /admin:文章/分类/标签 CRUD,JSON 文件存储
- 登录认证(cookie session)
- 设计系统文档 UI.md
2026-06-24 08:47:14 +08:00

188 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}