完善角色与消息权限:收紧板管内容处置,彻底删除仅站长,并统一管理删帖/评弹窗。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 14:30:08 +08:00
parent 1f2e4b2a33
commit c0462a3500
37 changed files with 1340 additions and 472 deletions

View File

@@ -41,6 +41,7 @@ import {
type AdminContentStatus,
type PublicSettings,
} from "@/lib/api";
import { canModerateAuthor } from "@/lib/roles";
import { formatRelative } from "@/lib/format";
import { toast } from "@/lib/toast";
import { realtime, RT_MODERATION_CHANGED } from "@/lib/realtime";
@@ -290,6 +291,8 @@ export default function ContentAdmin({
initialCounts,
initError,
canConfigureSettings,
canPurge,
viewerRole,
initialSettings,
}: {
initialKind: ContentKind;
@@ -301,6 +304,8 @@ export default function ContentAdmin({
initialCounts: AdminContentCounts;
initError: string;
canConfigureSettings: boolean;
canPurge: boolean;
viewerRole: string;
initialSettings: PublicSettings;
}) {
// 窄屏分段:lg 以下只显示一栏;PC 双栏同时可见
@@ -663,46 +668,50 @@ export default function ContentAdmin({
) : null}
{p.deleted ? (
<>
<ContentAction
label="恢复"
ok
disabled={busy}
icon={
busy ? (
<Loader2 size={13} className="animate-spin" />
) : (
<RotateCcw size={13} />
)
}
onClick={() =>
runAction(
key,
() => apiAdminRestorePost(p.id),
"帖子已恢复",
() => reloadPosts()
)
}
/>
<ContentAction
label="清除"
danger
disabled={busy}
icon={<Trash2 size={13} />}
onClick={() =>
askConfirm(
"彻底删除帖子",
`「${p.title}」将从数据库永久删除,且其下评论一并清除,不可撤销。`,
() =>
apiAdminPurgePost(p.id).then(() => {
toast("帖子已彻底删除", "ok");
reloadPosts();
}),
true
)
}
/>
{canModerateAuthor({ role: viewerRole }, p.user?.role) ? (
<ContentAction
label="恢复"
ok
disabled={busy}
icon={
busy ? (
<Loader2 size={13} className="animate-spin" />
) : (
<RotateCcw size={13} />
)
}
onClick={() =>
runAction(
key,
() => apiAdminRestorePost(p.id),
"帖子已恢复",
() => reloadPosts()
)
}
/>
) : null}
{canPurge ? (
<ContentAction
label="清除"
danger
disabled={busy}
icon={<Trash2 size={13} />}
onClick={() =>
askConfirm(
"彻底删除帖子",
`「${p.title}」将从数据库永久删除,且其下评论一并清除,不可撤销。`,
() =>
apiAdminPurgePost(p.id).then(() => {
toast("帖子已彻底删除", "ok");
reloadPosts();
}),
true
)
}
/>
) : null}
</>
) : (
) : canModerateAuthor({ role: viewerRole }, p.user?.role) ? (
<ContentAction
label="删除"
danger
@@ -710,7 +719,7 @@ export default function ContentAdmin({
icon={<Trash2 size={13} />}
onClick={() => setStaffDeletePost(p)}
/>
)}
) : null}
</div>
</div>
</div>
@@ -803,46 +812,50 @@ export default function ContentAdmin({
/>
{c.deleted ? (
<>
<ContentAction
label="恢复"
ok
disabled={busy}
icon={
busy ? (
<Loader2 size={13} className="animate-spin" />
) : (
<RotateCcw size={13} />
)
}
onClick={() =>
runAction(
key,
() => apiAdminRestoreComment(c.id),
"评论已恢复",
() => reloadComments()
)
}
/>
<ContentAction
label="清除"
danger
disabled={busy}
icon={<Trash2 size={13} />}
onClick={() =>
askConfirm(
"彻底删除评论",
"该评论将永久删除且不可恢复。",
() =>
apiAdminPurgeComment(c.id).then(() => {
toast("评论已彻底删除", "ok");
reloadComments();
}),
true
)
}
/>
{canModerateAuthor({ role: viewerRole }, c.user?.role) ? (
<ContentAction
label="恢复"
ok
disabled={busy}
icon={
busy ? (
<Loader2 size={13} className="animate-spin" />
) : (
<RotateCcw size={13} />
)
}
onClick={() =>
runAction(
key,
() => apiAdminRestoreComment(c.id),
"评论已恢复",
() => reloadComments()
)
}
/>
) : null}
{canPurge ? (
<ContentAction
label="清除"
danger
disabled={busy}
icon={<Trash2 size={13} />}
onClick={() =>
askConfirm(
"彻底删除评论",
"该评论将永久删除且不可恢复。",
() =>
apiAdminPurgeComment(c.id).then(() => {
toast("评论已彻底删除", "ok");
reloadComments();
}),
true
)
}
/>
) : null}
</>
) : (
) : canModerateAuthor({ role: viewerRole }, c.user?.role) ? (
<ContentAction
label="删除"
danger
@@ -850,7 +863,7 @@ export default function ContentAdmin({
icon={<Trash2 size={13} />}
onClick={() => setStaffDeleteComment(c)}
/>
)}
) : null}
</div>
</div>
</div>

View File

@@ -10,7 +10,7 @@ import {
type AdminContentStatus,
} from "@/lib/api";
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
import { canModerateAny, isSuperOrOwner } from "@/lib/roles";
import { canModerateAny, isSuperOrOwner, isOwner } from "@/lib/roles";
import Forbidden from "../Forbidden";
import ContentAdmin, { type ContentKind } from "./ContentAdmin";
@@ -93,6 +93,8 @@ export default async function AdminContentPage({ searchParams }: ContentPageProp
initialCounts={counts}
initError={initError}
canConfigureSettings={isSuperOrOwner(me.user.role)}
canPurge={isOwner(me.user.role)}
viewerRole={me.user.role}
initialSettings={settings}
/>
);