fix: 修复后台文章计数为0 + 分类编辑补全描述字段 + CSP放行外部图片 + 更新关于页描述

- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total
- 分类管理:编辑模式新增描述输入框,保存时一并提交 description
- CSP:img-src 加入 https: 允许加载外部图片
- 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库
- Footer:添加 ICP 备案号
This commit is contained in:
胡旭
2026-06-24 13:51:48 +08:00
parent 3707eddfd4
commit 18e915bcbb
69 changed files with 6818 additions and 1422 deletions
+32 -154
View File
@@ -3,185 +3,63 @@
import { useState, useEffect } from "react";
import { useRouter, useParams } from "next/navigation";
import type { Post, Category, Tag } from "@/lib/store";
import { useToast, safeFetch } from "@/components/Toast";
import PostForm from "@/components/admin/PostForm";
import type { PostFormData } from "@/components/admin/PostForm";
export default function EditPostPage() {
const router = useRouter();
const params = useParams();
const id = params.id as string;
const { toast } = useToast();
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]) => {
safeFetch(`/api/posts/${id}`, undefined, toast).then((r) => r.json()),
safeFetch("/api/categories", undefined, toast).then((r) => r.json()),
safeFetch("/api/tags", undefined, toast).then((r) => r.json()),
]).then(([p, cats, tgs]) => {
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]);
setCategories(cats);
setAllTags(tgs);
}).catch(() => {})
.finally(() => setLoading(false));
}, [id, toast]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const res = await fetch(`/api/posts/${id}`, {
async function handleSubmit(data: PostFormData) {
await safeFetch(`/api/posts/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (res.ok) router.push("/admin/posts");
body: JSON.stringify(data),
}, toast);
toast("文章已更新", "success");
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 (loading) return <div className="font-sans text-muted-foreground">...</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>
<button onClick={() => router.back()} className="font-sans text-sm text-muted-foreground hover:text-foreground transition-colors">
</button>
<h1 className="font-display text-3xl font-medium"></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>
<PostForm
mode="edit"
initialData={post}
categories={categories}
tags={allTags}
onSubmit={handleSubmit}
onCancel={() => router.back()}
/>
</div>
);
}