feat: 评论/帖子编辑历史与编辑标记,内容锁定
- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal - 新增 PostEditedMark 编辑标记展示 - 新增内容锁定机制(content_lock)防止并发编辑冲突 - 审核与通知服务适配
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Pencil, Trash2, Pin, Star, SlidersHorizontal, ChevronDown, Check, BadgeCheck } from "lucide-react";
|
||||
import { apiMe, apiDeletePost, apiTogglePin, apiToggleRecommend, apiAdminApprovePost, type User } from "@/lib/api";
|
||||
import { canStaffSoftDelete, canModerateBoard, isAdminOrAbove } from "@/lib/roles";
|
||||
import { Pencil, Trash2, Pin, Star, SlidersHorizontal, ChevronDown, Check, BadgeCheck, Lock, LockOpen } from "lucide-react";
|
||||
import { apiMe, apiDeletePost, apiTogglePin, apiToggleRecommend, apiTogglePostLock, apiAdminApprovePost, type User } from "@/lib/api";
|
||||
import { canStaffSoftDelete, canModerateBoard, isAdminOrAbove, isStaff } from "@/lib/roles";
|
||||
import { toast } from "@/lib/toast";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog";
|
||||
import StaffDeletePostModal from "@/components/StaffDeletePostModal";
|
||||
@@ -19,6 +19,12 @@ interface PostActionsProps {
|
||||
recommended: boolean;
|
||||
/** 帖子审核状态:pending 时对可审者显示「通过评审」 */
|
||||
status: string;
|
||||
/** 帖子创建时间:自动编辑锁计时基准(staff 豁免) */
|
||||
createdAt: string;
|
||||
/** 帖子是否被管理员手动锁定(staff 豁免) */
|
||||
locked: boolean;
|
||||
/** 帖子可编辑时长(小时);0=不锁定;staff 豁免 */
|
||||
postEditLockHours: number;
|
||||
/** 供删除确认展示 */
|
||||
title?: string;
|
||||
}
|
||||
@@ -31,6 +37,9 @@ export default function PostActions({
|
||||
pinned,
|
||||
recommended,
|
||||
status,
|
||||
createdAt,
|
||||
locked,
|
||||
postEditLockHours,
|
||||
title,
|
||||
}: PostActionsProps) {
|
||||
const router = useRouter();
|
||||
@@ -39,6 +48,7 @@ export default function PostActions({
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [isPinned, setIsPinned] = useState(pinned > 0);
|
||||
const [isRecommended, setIsRecommended] = useState(recommended);
|
||||
const [isLocked, setIsLocked] = useState(locked);
|
||||
const [acting, setActing] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [staffDeleteOpen, setStaffDeleteOpen] = useState(false);
|
||||
@@ -70,6 +80,13 @@ export default function PostActions({
|
||||
!!user &&
|
||||
(user.id === authorId || canStaffSoftDelete(user, boardId, authorRole));
|
||||
const isAdmin = !!user && isAdminOrAbove(user.role);
|
||||
const staffUser = !!user && isStaff(user.role);
|
||||
// 编辑入口:手动锁帖或超过可编辑时限时对普通用户隐藏,staff 豁免
|
||||
const editTimeLocked =
|
||||
!staffUser &&
|
||||
postEditLockHours > 0 &&
|
||||
Date.now() - new Date(createdAt).getTime() > postEditLockHours * 3_600_000;
|
||||
const showEdit = canManage && !(isLocked && !staffUser) && !editTimeLocked;
|
||||
const canApprove =
|
||||
!!user && status === "pending" && canModerateBoard(user, boardId);
|
||||
// staff 删他人帖:走类型+理由;作者自删:简单确认
|
||||
@@ -139,6 +156,21 @@ export default function PostActions({
|
||||
}
|
||||
};
|
||||
|
||||
const handleLock = async () => {
|
||||
setActing(true);
|
||||
setOpen(false);
|
||||
try {
|
||||
const res = await apiTogglePostLock(postId);
|
||||
setIsLocked(res.locked);
|
||||
toast(res.locked ? "已锁定,普通用户不可编辑与回复" : "已解除锁定", "ok");
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast("操作失败,请稍后重试");
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!canManage) return null;
|
||||
|
||||
const itemCls =
|
||||
@@ -212,18 +244,35 @@ export default function PostActions({
|
||||
<span className="flex-1 text-left">{isRecommended ? "取消推荐" : "推荐"}</span>
|
||||
{isRecommended && <Check size={14} className="shrink-0" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={handleLock}
|
||||
className={itemCls}
|
||||
style={isLocked ? { color: "var(--danger)" } : { color: "var(--ink)" }}
|
||||
>
|
||||
{isLocked ? (
|
||||
<LockOpen size={14} className="shrink-0" />
|
||||
) : (
|
||||
<Lock size={14} className="shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 text-left">{isLocked ? "解除锁定" : "锁定"}</span>
|
||||
{isLocked && <Check size={14} className="shrink-0" />}
|
||||
</button>
|
||||
<div style={{ height: 1, background: "var(--line)" }} />
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href={`/post/${postId}/edit`}
|
||||
role="menuitem"
|
||||
className={`${itemCls} items-center`}
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
<Pencil size={14} className="shrink-0" />
|
||||
<span className="flex-1 text-left">编辑帖子</span>
|
||||
</Link>
|
||||
{showEdit && (
|
||||
<Link
|
||||
href={`/post/${postId}/edit`}
|
||||
role="menuitem"
|
||||
className={`${itemCls} items-center`}
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
<Pencil size={14} className="shrink-0" />
|
||||
<span className="flex-1 text-left">编辑帖子</span>
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
|
||||
Reference in New Issue
Block a user