feat(moderation): 帖评删除占位感知与硬删清场

管理删帖须填类型与理由并展示专用页;评论软删占位、版主可硬删不占楼。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 07:01:04 +08:00
parent 2b27703e6c
commit bcb4f6a603
15 changed files with 825 additions and 116 deletions

View File

@@ -8,6 +8,7 @@ import { apiMe, apiDeletePost, apiTogglePin, apiToggleRecommend, apiAdminApprove
import { canModerateBoard, isAdminOrAbove } from "@/lib/roles";
import { toast } from "@/lib/toast";
import ConfirmDialog from "@/components/ConfirmDialog";
import StaffDeletePostModal from "@/components/StaffDeletePostModal";
interface PostActionsProps {
postId: string;
@@ -17,6 +18,8 @@ interface PostActionsProps {
recommended: boolean;
/** 帖子审核状态:pending 时对可审者显示「通过评审」 */
status: string;
/** 供删除确认展示 */
title?: string;
}
export default function PostActions({
@@ -26,6 +29,7 @@ export default function PostActions({
pinned,
recommended,
status,
title,
}: PostActionsProps) {
const router = useRouter();
const [user, setUser] = useState<User | null>(null);
@@ -35,6 +39,7 @@ export default function PostActions({
const [isRecommended, setIsRecommended] = useState(recommended);
const [acting, setActing] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [staffDeleteOpen, setStaffDeleteOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -43,7 +48,6 @@ export default function PostActions({
.catch(() => setUser(null));
}, []);
// 点击外部 / Esc 关闭菜单(与通知铃铛同一交互模式)
useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
@@ -60,17 +64,20 @@ export default function PostActions({
};
}, [open]);
// 作者本人或有该板块审核权的管理团队可管理;置顶/加精仅管理员及以上
const canManage = !!user && (user.id === authorId || canModerateBoard(user, boardId));
const isAdmin = !!user && isAdminOrAbove(user.role);
const canApprove = !!user && status === "pending" && canModerateBoard(user, boardId);
// staff 删他人帖:走类型+理由;作者自删:简单确认
const staffDeletingOther =
!!user && user.id !== authorId && canModerateBoard(user, boardId);
const handleDelete = async () => {
const runDelete = async (opts?: { delete_type?: string; delete_reason?: string }) => {
setDeleting(true);
try {
const res = await apiDeletePost(postId);
const res = await apiDeletePost(postId, opts);
if (res.message) {
setConfirmDelete(false);
setStaffDeleteOpen(false);
router.push("/");
toast("帖子已删除", "ok");
} else {
@@ -217,7 +224,8 @@ export default function PostActions({
role="menuitem"
onClick={() => {
setOpen(false);
setConfirmDelete(true);
if (staffDeletingOther) setStaffDeleteOpen(true);
else setConfirmDelete(true);
}}
disabled={deleting}
className={`${itemCls} items-center`}
@@ -232,13 +240,21 @@ export default function PostActions({
<ConfirmDialog
open={confirmDelete}
title="删除帖子"
message="确定要删除这篇帖子吗?此操作不可恢复。"
message="确定要删除这篇帖子吗?列表将不再展示,打开原链接会提示已删除。"
confirmLabel="确认删除"
danger
busy={deleting}
onConfirm={handleDelete}
onConfirm={() => void runDelete()}
onCancel={() => setConfirmDelete(false)}
/>
<StaffDeletePostModal
open={staffDeleteOpen}
title={title}
busy={deleting}
onClose={() => setStaffDeleteOpen(false)}
onConfirm={(opts) => void runDelete(opts)}
/>
</div>
);
}