Files
jiang13-bbs/frontend/app/post/[id]/edit/page.tsx
freefire a7b6421840 feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系
- 实现实时权限快照加载与细粒度权限校验
- 新增内容审核队列与前后端页面
- 重构用户权限管理与站点管理逻辑
- 新增评论/帖子审核状态与通知推送
- 优化前端权限控制与角色徽章展示
- 修正数据库迁移与默认值问题
2026-09-15 04:47:34 +08:00

153 lines
4.4 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 } from "next/navigation";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { fetchPostDetail, apiMe, apiUpdatePost, type Post, type User } from "@/lib/api";
import { canModerateBoard } from "@/lib/roles";
interface EditPageProps {
params: Promise<{ id: string }>;
}
export default function EditPostPage({ params }: EditPageProps) {
const router = useRouter();
const [post, setPost] = useState<Post | null>(null);
const [user, setUser] = useState<User | null>(null);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [tags, setTags] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [checking, setChecking] = useState(true);
const [id, setId] = useState("");
useEffect(() => {
params.then((p) => setId(p.id));
}, [params]);
useEffect(() => {
if (!id) return;
Promise.all([fetchPostDetail(id), apiMe()])
.then(([postData, meData]) => {
setPost(postData.post);
setTitle(postData.post.title);
setContent(postData.post.content);
setTags(postData.post.tags || "");
const u = meData.user;
setUser(u || null);
if (
!u ||
(u.id !== postData.post.user.id && !canModerateBoard(u, postData.post.board_id))
) {
router.push(`/post/${id}`);
}
})
.catch(() => router.push(`/post/${id}`))
.finally(() => setChecking(false));
}, [id, router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!post) return;
setLoading(true);
setError("");
try {
const res = await apiUpdatePost(String(post.id), { title, content, tags });
if (res.post) {
router.push(`/post/${post.id}`);
} else {
setError(res.error || "更新失败");
}
} finally {
setLoading(false);
}
};
if (checking) {
return (
<div className="max-w-3xl mx-auto text-center py-20">
<p className="meta">正在加载帖子…</p>
</div>
);
}
if (!post) return null;
return (
<div className="max-w-3xl mx-auto">
<nav className="meta mb-5 flex items-center gap-1.5 flex-wrap text-[13px]">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
<ChevronRight size={13} />
<Link href={`/post/${post.id}`} className="hover:text-[var(--accent)] transition-colors truncate max-w-[12rem]">
帖子详情
</Link>
<ChevronRight size={13} />
<span style={{ color: "var(--accent)" }}>编辑</span>
</nav>
<div className="mb-7">
<h1 className="text-[26px] sm:text-[30px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
编辑帖子
</h1>
</div>
<form onSubmit={handleSubmit} className="panel p-5 sm:p-7 space-y-5">
{error && <div className="alert-error">{error}</div>}
<div>
<label className="field-label">标题</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
maxLength={256}
className="field"
/>
</div>
<div>
<label className="field-label">标签(逗号分隔,可选)</label>
<input
type="text"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="Go, PostgreSQL, 架构"
className="field"
/>
</div>
<div>
<label className="field-label">正文(支持 Markdown)</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
required
rows={14}
className="field"
/>
</div>
<div className="flex gap-3 pt-2">
<button
type="submit"
disabled={loading || !title.trim() || !content.trim()}
className="btn btn-primary"
>
{loading ? "保存中..." : "保存修改"}
</button>
<button
type="button"
onClick={() => router.push(`/post/${post.id}`)}
className="btn btn-line"
>
取消
</button>
</div>
</form>
</div>
);
}