feat(admin): 合并内容审核到内容管理,并固定后台侧栏
待审与内容库同一页处理;筛选切换去掉加载占位,避免页面闪动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Check,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
History,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
MessagesSquare,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
apiAdminContentCounts,
|
||||
@@ -21,6 +23,10 @@ import {
|
||||
apiAdminSoftDeleteComment,
|
||||
apiAdminRestoreComment,
|
||||
apiAdminPurgeComment,
|
||||
apiAdminApprovePost,
|
||||
apiAdminRejectPost,
|
||||
apiAdminApproveComment,
|
||||
apiAdminRejectComment,
|
||||
apiUpdateSiteSettings,
|
||||
type AdminContentComment,
|
||||
type AdminContentCounts,
|
||||
@@ -30,6 +36,7 @@ import {
|
||||
} from "@/lib/api";
|
||||
import { formatRelative } from "@/lib/format";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { realtime, RT_MODERATION_CHANGED } from "@/lib/realtime";
|
||||
import Avatar from "@/components/Avatar";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog";
|
||||
import StaffDeletePostModal from "@/components/StaffDeletePostModal";
|
||||
@@ -44,13 +51,14 @@ import {
|
||||
AdminSegmented,
|
||||
AdminSettingRow,
|
||||
AdminStatusChip,
|
||||
AdminStepper,
|
||||
AdminSwitch,
|
||||
contentStatusChip,
|
||||
} from "@/components/admin";
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
type Kind = "posts" | "comments";
|
||||
export type ContentKind = "posts" | "comments";
|
||||
|
||||
const STATUS_TABS: { key: AdminContentStatus; label: string }[] = [
|
||||
{ key: "live", label: "全部" },
|
||||
@@ -70,31 +78,75 @@ function excerpt(md: string, max = 120): string {
|
||||
return text.length > max ? `${text.slice(0, max)}…` : text;
|
||||
}
|
||||
|
||||
function isPendingRow(status: string, deleted: boolean) {
|
||||
return !deleted && status === "pending";
|
||||
}
|
||||
|
||||
function PendingActions({
|
||||
busy,
|
||||
onApprove,
|
||||
onReject,
|
||||
}: {
|
||||
busy: boolean;
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="btn btn-sm btn-ok" disabled={busy} onClick={onApprove}>
|
||||
{busy ? <Loader2 size={13} className="animate-spin" /> : <Check size={13} />}
|
||||
通过
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-danger" disabled={busy} onClick={onReject}>
|
||||
<X size={13} /> 拒绝
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function emptyCopy(kind: ContentKind, status: AdminContentStatus): { title: string; description?: string } {
|
||||
if (status === "pending") {
|
||||
return {
|
||||
title: kind === "posts" ? "暂无待审帖子" : "暂无待审评论",
|
||||
description: "新提交的内容会按时间顺序出现在这里,可通过或拒绝",
|
||||
};
|
||||
}
|
||||
return { title: kind === "posts" ? "暂无帖子" : "暂无评论" };
|
||||
}
|
||||
|
||||
export default function ContentAdmin({
|
||||
initialKind,
|
||||
initialStatus,
|
||||
initialPosts,
|
||||
initialPostsTotal,
|
||||
initialComments,
|
||||
initialCommentsTotal,
|
||||
initialCounts,
|
||||
initError,
|
||||
canConfigureSettings,
|
||||
initialSettings,
|
||||
}: {
|
||||
initialKind: ContentKind;
|
||||
initialStatus: AdminContentStatus;
|
||||
initialPosts: AdminContentPost[];
|
||||
initialPostsTotal: number;
|
||||
initialComments: AdminContentComment[];
|
||||
initialCommentsTotal: number;
|
||||
initialCounts: AdminContentCounts;
|
||||
initError: string;
|
||||
canConfigureSettings: boolean;
|
||||
initialSettings: PublicSettings;
|
||||
}) {
|
||||
const [kind, setKind] = useState<Kind>("posts");
|
||||
const [status, setStatus] = useState<AdminContentStatus>("live");
|
||||
const [kind, setKind] = useState<ContentKind>(initialKind);
|
||||
const [status, setStatus] = useState<AdminContentStatus>(initialStatus);
|
||||
const [counts, setCounts] = useState(initialCounts);
|
||||
|
||||
const [posts, setPosts] = useState(initialPosts);
|
||||
const [postsTotal, setPostsTotal] = useState(initialPostsTotal);
|
||||
const [postsPage, setPostsPage] = useState(1);
|
||||
|
||||
const [comments, setComments] = useState<AdminContentComment[]>([]);
|
||||
const [commentsTotal, setCommentsTotal] = useState(0);
|
||||
const [comments, setComments] = useState(initialComments);
|
||||
const [commentsTotal, setCommentsTotal] = useState(initialCommentsTotal);
|
||||
const [commentsPage, setCommentsPage] = useState(1);
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
@@ -111,6 +163,7 @@ export default function ContentAdmin({
|
||||
title: string;
|
||||
message: string;
|
||||
danger?: boolean;
|
||||
confirmLabel?: string;
|
||||
run: () => Promise<void>;
|
||||
} | null>(null);
|
||||
const [confirmBusy, setConfirmBusy] = useState(false);
|
||||
@@ -131,6 +184,7 @@ export default function ContentAdmin({
|
||||
setPosts(res.posts);
|
||||
setPostsTotal(res.total);
|
||||
setPostsPage(page);
|
||||
setKind("posts");
|
||||
setError("");
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "获取帖子失败");
|
||||
@@ -146,6 +200,7 @@ export default function ContentAdmin({
|
||||
setComments(res.comments);
|
||||
setCommentsTotal(res.total);
|
||||
setCommentsPage(page);
|
||||
setKind("comments");
|
||||
setError("");
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "获取评论失败");
|
||||
@@ -160,13 +215,23 @@ export default function ContentAdmin({
|
||||
refreshCounts();
|
||||
};
|
||||
|
||||
const reloadAfterItemChange = () => {
|
||||
const p = kind === "posts" ? postsPage : commentsPage;
|
||||
const len = kind === "posts" ? posts.length : comments.length;
|
||||
reload(len <= 1 && p > 1 ? p - 1 : p);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshCounts();
|
||||
}, [refreshCounts]);
|
||||
|
||||
const switchKind = (k: Kind) => {
|
||||
useEffect(() => {
|
||||
const off = realtime.on(RT_MODERATION_CHANGED, refreshCounts);
|
||||
return off;
|
||||
}, [refreshCounts]);
|
||||
|
||||
const switchKind = (k: ContentKind) => {
|
||||
if (k === kind) return;
|
||||
setKind(k);
|
||||
setError("");
|
||||
if (k === "posts") void loadPosts(1, status, appliedQ);
|
||||
else void loadComments(1, status, appliedQ);
|
||||
@@ -198,7 +263,7 @@ export default function ContentAdmin({
|
||||
fn()
|
||||
.then(() => {
|
||||
toast(okMsg, "ok");
|
||||
reload(kind === "posts" ? postsPage : commentsPage);
|
||||
reloadAfterItemChange();
|
||||
})
|
||||
.catch((e) => toast(e instanceof Error ? e.message : "操作失败"))
|
||||
.finally(() => setActingKey(null));
|
||||
@@ -208,8 +273,24 @@ export default function ContentAdmin({
|
||||
title: string,
|
||||
message: string,
|
||||
run: () => Promise<void>,
|
||||
danger = false
|
||||
) => setConfirm({ title, message, run, danger });
|
||||
danger = false,
|
||||
confirmLabel?: string
|
||||
) => setConfirm({ title, message, run, danger, confirmLabel });
|
||||
|
||||
const askReject = (kindNow: ContentKind, id: number) => {
|
||||
askConfirm(
|
||||
kindNow === "posts" ? "拒绝这篇帖子?" : "拒绝这条评论?",
|
||||
"拒绝后作者将收到通知,内容不会公开。",
|
||||
async () => {
|
||||
if (kindNow === "posts") await apiAdminRejectPost(id);
|
||||
else await apiAdminRejectComment(id);
|
||||
toast("已拒绝,作者将收到通知", "ok");
|
||||
reloadAfterItemChange();
|
||||
},
|
||||
true,
|
||||
"确认拒绝"
|
||||
);
|
||||
};
|
||||
|
||||
const toggleTrust = async () => {
|
||||
if (!canConfigureSettings || savingPolicy) return;
|
||||
@@ -254,12 +335,18 @@ export default function ContentAdmin({
|
||||
if (st === "pending") return counts.posts_pending;
|
||||
if (st === "rejected") return counts.posts_rejected;
|
||||
if (st === "deleted") return counts.posts_deleted;
|
||||
if (st === "published") {
|
||||
return Math.max(0, counts.posts_live - counts.posts_pending - counts.posts_rejected);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (st === "live") return counts.comments_live;
|
||||
if (st === "pending") return counts.comments_pending;
|
||||
if (st === "rejected") return counts.comments_rejected;
|
||||
if (st === "deleted") return counts.comments_deleted;
|
||||
if (st === "published") {
|
||||
return Math.max(0, counts.comments_live - counts.comments_pending - counts.comments_rejected);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -268,12 +355,14 @@ export default function ContentAdmin({
|
||||
else void loadComments(p, status, appliedQ);
|
||||
};
|
||||
|
||||
const empty = emptyCopy(kind, status);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPageHeader
|
||||
icon={FolderOpen}
|
||||
title="内容管理"
|
||||
description="浏览与处理帖子/评论,含回收站恢复与彻底删除;策略开关仅超管/站长可改"
|
||||
description="审核待发布内容,并处理已发布/已拒绝条目与回收站;策略开关仅超管/站长可改"
|
||||
/>
|
||||
|
||||
{canConfigureSettings && (
|
||||
@@ -294,88 +383,104 @@ export default function ContentAdmin({
|
||||
<AdminSettingRow
|
||||
title="新用户发帖冷静期"
|
||||
description="注册后满该小时数才能发帖;设为 0 表示关闭。上限 168 小时(7 天)。"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={168}
|
||||
className="field w-28"
|
||||
value={cooldown}
|
||||
onChange={(e) => setCooldown(Number(e.target.value))}
|
||||
aria-label="冷静期小时数"
|
||||
/>
|
||||
<span className="meta text-[12.5px]">小时</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={savingPolicy}
|
||||
onClick={() => void saveCooldown()}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</AdminSettingRow>
|
||||
control={
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AdminStepper
|
||||
value={cooldown}
|
||||
onChange={setCooldown}
|
||||
min={0}
|
||||
max={168}
|
||||
unit="小时"
|
||||
ariaLabel="冷静期小时数"
|
||||
disabled={savingPolicy}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={savingPolicy}
|
||||
onClick={() => void saveCooldown()}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="panel overflow-hidden">
|
||||
<div className="admin-toolbar">
|
||||
<AdminSegmented<Kind>
|
||||
ariaLabel="内容类型"
|
||||
value={kind}
|
||||
onChange={switchKind}
|
||||
options={[
|
||||
{
|
||||
key: "posts",
|
||||
label: "帖子",
|
||||
icon: <FileText size={14} />,
|
||||
count: counts.posts_live,
|
||||
},
|
||||
{
|
||||
key: "comments",
|
||||
label: "评论",
|
||||
icon: <MessagesSquare size={14} />,
|
||||
count: counts.comments_live,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<AdminSegmented<AdminContentStatus>
|
||||
ariaLabel="状态筛选"
|
||||
size="sm"
|
||||
value={status}
|
||||
onChange={switchStatus}
|
||||
options={STATUS_TABS.map((t) => ({
|
||||
key: t.key,
|
||||
label: t.label,
|
||||
count: statusCount(t.key) ?? undefined,
|
||||
}))}
|
||||
/>
|
||||
<AdminSearchField
|
||||
value={keyword}
|
||||
onChange={setKeyword}
|
||||
onSubmit={applySearch}
|
||||
onClear={clearSearch}
|
||||
placeholder={kind === "posts" ? "搜索帖子标题…" : "搜索评论正文…"}
|
||||
ariaLabel="搜索内容"
|
||||
/>
|
||||
<div className="admin-content-toolbar">
|
||||
<div className="admin-content-kind" role="tablist" aria-label="内容类型">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={kind === "posts"}
|
||||
className="admin-content-kind-btn"
|
||||
onClick={() => switchKind("posts")}
|
||||
>
|
||||
<FileText size={14} aria-hidden />
|
||||
帖子
|
||||
<span className="admin-content-kind-count">{counts.posts_live}</span>
|
||||
{counts.posts_pending > 0 ? (
|
||||
<span className="admin-status is-warn" title={`${counts.posts_pending} 条待审`}>
|
||||
{counts.posts_pending}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={kind === "comments"}
|
||||
className="admin-content-kind-btn"
|
||||
onClick={() => switchKind("comments")}
|
||||
>
|
||||
<MessagesSquare size={14} aria-hidden />
|
||||
评论
|
||||
<span className="admin-content-kind-count">{counts.comments_live}</span>
|
||||
{counts.comments_pending > 0 ? (
|
||||
<span className="admin-status is-warn" title={`${counts.comments_pending} 条待审`}>
|
||||
{counts.comments_pending}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
<span className="admin-content-split" aria-hidden />
|
||||
<div className="admin-content-status">
|
||||
<AdminSegmented<AdminContentStatus>
|
||||
ariaLabel="状态筛选"
|
||||
size="sm"
|
||||
value={status}
|
||||
onChange={switchStatus}
|
||||
options={STATUS_TABS.map((t) => ({
|
||||
key: t.key,
|
||||
label: t.label,
|
||||
count: statusCount(t.key) ?? undefined,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-content-search">
|
||||
<AdminSearchField
|
||||
value={keyword}
|
||||
onChange={setKeyword}
|
||||
onSubmit={applySearch}
|
||||
onClear={clearSearch}
|
||||
placeholder={kind === "posts" ? "搜索帖子标题…" : "搜索评论正文…"}
|
||||
ariaLabel="搜索内容"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-16 meta text-[13px]">
|
||||
<Loader2 size={16} className="animate-spin" /> 加载中…
|
||||
</div>
|
||||
) : error ? (
|
||||
<p className="alert-error m-4">{error}</p>
|
||||
) : kind === "posts" && posts.length === 0 ? (
|
||||
<AdminEmpty icon={<FileText size={22} />} title="暂无帖子" />
|
||||
{error ? <p className="alert-error m-4">{error}</p> : null}
|
||||
{kind === "posts" && posts.length === 0 ? (
|
||||
<AdminEmpty icon={<FileText size={22} />} title={empty.title} description={empty.description} />
|
||||
) : kind === "comments" && comments.length === 0 ? (
|
||||
<AdminEmpty icon={<MessagesSquare size={22} />} title="暂无评论" />
|
||||
<AdminEmpty icon={<MessagesSquare size={22} />} title={empty.title} description={empty.description} />
|
||||
) : kind === "posts" ? (
|
||||
posts.map((p, idx) => {
|
||||
const key = `p-${p.id}`;
|
||||
const busy = actingKey === key;
|
||||
const chip = contentStatusChip(p.status, p.deleted);
|
||||
const pending = isPendingRow(p.status, p.deleted);
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
@@ -412,7 +517,16 @@ export default function ContentAdmin({
|
||||
{p.user?.nickname} · {p.comment_count} 评
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<div className="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">
|
||||
{pending && (
|
||||
<PendingActions
|
||||
busy={busy}
|
||||
onApprove={() =>
|
||||
runAction(key, () => apiAdminApprovePost(p.id), "已通过审核,内容已公开")
|
||||
}
|
||||
onReject={() => askReject("posts", p.id)}
|
||||
/>
|
||||
)}
|
||||
{p.deleted ? (
|
||||
<>
|
||||
<AdminIconButton
|
||||
@@ -441,7 +555,7 @@ export default function ContentAdmin({
|
||||
() =>
|
||||
apiAdminPurgePost(p.id).then(() => {
|
||||
toast("帖子已彻底删除", "ok");
|
||||
reload(postsPage);
|
||||
reloadAfterItemChange();
|
||||
}),
|
||||
true
|
||||
)
|
||||
@@ -469,6 +583,7 @@ export default function ContentAdmin({
|
||||
const key = `c-${c.id}`;
|
||||
const busy = actingKey === key;
|
||||
const chip = contentStatusChip(c.status, c.deleted);
|
||||
const pending = isPendingRow(c.status, c.deleted);
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
@@ -506,7 +621,16 @@ export default function ContentAdmin({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<div className="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">
|
||||
{pending && (
|
||||
<PendingActions
|
||||
busy={busy}
|
||||
onApprove={() =>
|
||||
runAction(key, () => apiAdminApproveComment(c.id), "已通过审核,内容已公开")
|
||||
}
|
||||
onReject={() => askReject("comments", c.id)}
|
||||
/>
|
||||
)}
|
||||
<AdminIconButton
|
||||
tip="历史"
|
||||
ariaLabel="查看编辑历史"
|
||||
@@ -542,7 +666,7 @@ export default function ContentAdmin({
|
||||
() =>
|
||||
apiAdminPurgeComment(c.id).then(() => {
|
||||
toast("评论已彻底删除", "ok");
|
||||
reload(commentsPage);
|
||||
reloadAfterItemChange();
|
||||
}),
|
||||
true
|
||||
)
|
||||
@@ -582,7 +706,7 @@ export default function ContentAdmin({
|
||||
message={confirm?.message || ""}
|
||||
danger={confirm?.danger}
|
||||
busy={confirmBusy}
|
||||
confirmLabel={confirm?.danger ? "确认删除" : "确定"}
|
||||
confirmLabel={confirm?.confirmLabel || (confirm?.danger ? "确认删除" : "确定")}
|
||||
onCancel={() => !confirmBusy && setConfirm(null)}
|
||||
onConfirm={() => {
|
||||
if (!confirm) return;
|
||||
@@ -609,7 +733,7 @@ export default function ContentAdmin({
|
||||
.then(() => {
|
||||
toast("已移入回收站", "ok");
|
||||
setStaffDeletePost(null);
|
||||
reload(postsPage);
|
||||
reloadAfterItemChange();
|
||||
})
|
||||
.catch((e) => toast(e instanceof Error ? e.message : "操作失败"))
|
||||
.finally(() => setStaffDeleteBusy(false));
|
||||
@@ -628,7 +752,7 @@ export default function ContentAdmin({
|
||||
.then(() => {
|
||||
toast("已移入回收站", "ok");
|
||||
setStaffDeleteComment(null);
|
||||
reload(commentsPage);
|
||||
reloadAfterItemChange();
|
||||
})
|
||||
.catch((e) => toast(e instanceof Error ? e.message : "操作失败"))
|
||||
.finally(() => setStaffDeleteCommentBusy(false));
|
||||
|
||||
@@ -4,18 +4,39 @@ import { authCookieHeader } from "@/lib/cookies";
|
||||
import {
|
||||
fetchAdminContentCounts,
|
||||
fetchAdminContentPosts,
|
||||
fetchAdminContentComments,
|
||||
type AdminContentComment,
|
||||
type AdminContentPost,
|
||||
type AdminContentStatus,
|
||||
} from "@/lib/api";
|
||||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||||
import { canModerateAny, isSuperOrOwner } from "@/lib/roles";
|
||||
import Forbidden from "../Forbidden";
|
||||
import ContentAdmin from "./ContentAdmin";
|
||||
import ContentAdmin, { type ContentKind } from "./ContentAdmin";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "内容管理",
|
||||
};
|
||||
|
||||
export default async function AdminContentPage() {
|
||||
const STATUSES: AdminContentStatus[] = ["live", "published", "pending", "rejected", "deleted"];
|
||||
|
||||
function parseStatus(v?: string): AdminContentStatus {
|
||||
return v && STATUSES.includes(v as AdminContentStatus) ? (v as AdminContentStatus) : "live";
|
||||
}
|
||||
|
||||
function parseKind(v?: string): ContentKind {
|
||||
return v === "comments" ? "comments" : "posts";
|
||||
}
|
||||
|
||||
interface ContentPageProps {
|
||||
searchParams: Promise<{ status?: string; kind?: string }>;
|
||||
}
|
||||
|
||||
export default async function AdminContentPage({ searchParams }: ContentPageProps) {
|
||||
const sp = await searchParams;
|
||||
const initialStatus = parseStatus(sp.status);
|
||||
const initialKind = parseKind(sp.kind);
|
||||
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const me = await getMeCached(cookie || undefined);
|
||||
if (!me.user || !canModerateAny(me.user)) {
|
||||
@@ -23,30 +44,49 @@ export default async function AdminContentPage() {
|
||||
}
|
||||
|
||||
let initialPosts: AdminContentPost[] = [];
|
||||
let initialTotal = 0;
|
||||
let initialPostsTotal = 0;
|
||||
let initialComments: AdminContentComment[] = [];
|
||||
let initialCommentsTotal = 0;
|
||||
let initError = "";
|
||||
const [postsSettled, counts] = await Promise.all([
|
||||
fetchAdminContentPosts(1, "live", cookie || undefined)
|
||||
.then((res) => ({ ok: true as const, res }))
|
||||
.catch((e) => ({
|
||||
ok: false as const,
|
||||
error: e instanceof Error ? e.message : "获取内容列表失败",
|
||||
})),
|
||||
const [listSettled, counts] = await Promise.all([
|
||||
initialKind === "comments"
|
||||
? fetchAdminContentComments(1, initialStatus, cookie || undefined)
|
||||
.then((res) => ({ ok: true as const, kind: "comments" as const, res }))
|
||||
.catch((e) => ({
|
||||
ok: false as const,
|
||||
error: e instanceof Error ? e.message : "获取内容列表失败",
|
||||
}))
|
||||
: fetchAdminContentPosts(1, initialStatus, cookie || undefined)
|
||||
.then((res) => ({ ok: true as const, kind: "posts" as const, res }))
|
||||
.catch((e) => ({
|
||||
ok: false as const,
|
||||
error: e instanceof Error ? e.message : "获取内容列表失败",
|
||||
})),
|
||||
fetchAdminContentCounts(cookie || undefined),
|
||||
]);
|
||||
if (postsSettled.ok) {
|
||||
initialPosts = postsSettled.res.posts;
|
||||
initialTotal = postsSettled.res.total;
|
||||
if (listSettled.ok) {
|
||||
if (listSettled.kind === "posts") {
|
||||
initialPosts = listSettled.res.posts;
|
||||
initialPostsTotal = listSettled.res.total;
|
||||
} else {
|
||||
initialComments = listSettled.res.comments;
|
||||
initialCommentsTotal = listSettled.res.total;
|
||||
}
|
||||
} else {
|
||||
initError = postsSettled.error;
|
||||
initError = listSettled.error;
|
||||
}
|
||||
|
||||
const settings = await getPublicSettingsCached();
|
||||
|
||||
return (
|
||||
<ContentAdmin
|
||||
key={`${initialKind}-${initialStatus}`}
|
||||
initialKind={initialKind}
|
||||
initialStatus={initialStatus}
|
||||
initialPosts={initialPosts}
|
||||
initialPostsTotal={initialTotal}
|
||||
initialPostsTotal={initialPostsTotal}
|
||||
initialComments={initialComments}
|
||||
initialCommentsTotal={initialCommentsTotal}
|
||||
initialCounts={counts}
|
||||
initError={initError}
|
||||
canConfigureSettings={isSuperOrOwner(me.user.role)}
|
||||
|
||||
Reference in New Issue
Block a user