polish(admin): 抽出后台控件包并统一各页布局与交互
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,15 +3,12 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
History,
|
||||
Loader2,
|
||||
MessagesSquare,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -38,6 +35,18 @@ import ConfirmDialog from "@/components/ConfirmDialog";
|
||||
import StaffDeletePostModal from "@/components/StaffDeletePostModal";
|
||||
import DeleteCommentModal from "@/components/DeleteCommentModal";
|
||||
import CommentEditHistoryModal from "@/components/CommentEditHistoryModal";
|
||||
import {
|
||||
AdminEmpty,
|
||||
AdminIconButton,
|
||||
AdminPageHeader,
|
||||
AdminPager,
|
||||
AdminSearchField,
|
||||
AdminSegmented,
|
||||
AdminSettingRow,
|
||||
AdminStatusChip,
|
||||
AdminSwitch,
|
||||
contentStatusChip,
|
||||
} from "@/components/admin";
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
@@ -51,14 +60,6 @@ const STATUS_TABS: { key: AdminContentStatus; label: string }[] = [
|
||||
{ key: "deleted", label: "已删除" },
|
||||
];
|
||||
|
||||
function statusLabel(status: string, deleted: boolean) {
|
||||
if (deleted) return "已删除";
|
||||
if (status === "pending") return "待审";
|
||||
if (status === "rejected") return "已拒绝";
|
||||
if (status === "published") return "已发布";
|
||||
return status;
|
||||
}
|
||||
|
||||
function excerpt(md: string, max = 120): string {
|
||||
const text = md
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
@@ -69,36 +70,6 @@ function excerpt(md: string, max = 120): string {
|
||||
return text.length > max ? `${text.slice(0, max)}…` : text;
|
||||
}
|
||||
|
||||
function Switch({
|
||||
checked,
|
||||
disabled,
|
||||
onChange,
|
||||
label,
|
||||
}: {
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: () => void;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
disabled={disabled}
|
||||
onClick={onChange}
|
||||
className="shrink-0 relative h-7 w-12 rounded-full transition-colors disabled:opacity-60"
|
||||
style={{ background: checked ? "var(--accent)" : "var(--line-2)" }}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 left-0.5 h-6 w-6 rounded-full bg-[var(--panel)] shadow transition-transform"
|
||||
style={{ transform: checked ? "translateX(1.25rem)" : "translateX(0)" }}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ContentAdmin({
|
||||
initialPosts,
|
||||
initialPostsTotal,
|
||||
@@ -209,14 +180,19 @@ export default function ContentAdmin({
|
||||
else void loadComments(1, st, appliedQ);
|
||||
};
|
||||
|
||||
const applySearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const applySearch = () => {
|
||||
const q = keyword.trim();
|
||||
setAppliedQ(q);
|
||||
if (kind === "posts") void loadPosts(1, status, q);
|
||||
else void loadComments(1, status, q);
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
setAppliedQ("");
|
||||
if (kind === "posts") void loadPosts(1, status, "");
|
||||
else void loadComments(1, status, "");
|
||||
};
|
||||
|
||||
const runAction = (key: string, fn: () => Promise<void>, okMsg: string) => {
|
||||
setActingKey(key);
|
||||
fn()
|
||||
@@ -271,7 +247,6 @@ export default function ContentAdmin({
|
||||
|
||||
const total = kind === "posts" ? postsTotal : commentsTotal;
|
||||
const page = kind === "posts" ? postsPage : commentsPage;
|
||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
|
||||
const statusCount = (st: AdminContentStatus) => {
|
||||
if (kind === "posts") {
|
||||
@@ -288,52 +263,38 @@ export default function ContentAdmin({
|
||||
return null;
|
||||
};
|
||||
|
||||
const goPage = (p: number) => {
|
||||
if (kind === "posts") void loadPosts(p, status, appliedQ);
|
||||
else void loadComments(p, status, appliedQ);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-5">
|
||||
<h1
|
||||
className="text-[22px] sm:text-[26px] font-extrabold tracking-tight flex items-center gap-2.5"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
<span
|
||||
className="w-9 h-9 rounded-xl flex items-center justify-center"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
<FolderOpen size={18} />
|
||||
</span>
|
||||
内容管理
|
||||
</h1>
|
||||
<p className="meta mt-1.5 text-[12.5px]">
|
||||
浏览与处理帖子/评论,含回收站恢复与彻底删除;策略开关仅超管/站长可改
|
||||
</p>
|
||||
</div>
|
||||
<AdminPageHeader
|
||||
icon={FolderOpen}
|
||||
title="内容管理"
|
||||
description="浏览与处理帖子/评论,含回收站恢复与彻底删除;策略开关仅超管/站长可改"
|
||||
/>
|
||||
|
||||
{canConfigureSettings && (
|
||||
<div className="grid gap-3 mb-5">
|
||||
<section className="panel p-4 sm:p-5 flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-[14.5px] font-semibold" style={{ color: "var(--ink)" }}>
|
||||
已过审用户后续免审
|
||||
</h2>
|
||||
<p className="meta mt-1 text-[12.5px] leading-relaxed">
|
||||
开启后,已有过审内容的用户后续发帖与评论将直接公开。默认开启。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={trustOn}
|
||||
disabled={savingPolicy}
|
||||
onChange={() => void toggleTrust()}
|
||||
label="已过审用户后续免审"
|
||||
/>
|
||||
</section>
|
||||
<div className="flex flex-col gap-3 mb-5">
|
||||
<AdminSettingRow
|
||||
title="已过审用户后续免审"
|
||||
description="开启后,已有过审内容的用户后续发帖与评论将直接公开。默认开启。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={trustOn}
|
||||
disabled={savingPolicy}
|
||||
onChange={() => void toggleTrust()}
|
||||
label="已过审用户后续免审"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="panel p-4 sm:p-5">
|
||||
<h2 className="text-[14.5px] font-semibold" style={{ color: "var(--ink)" }}>
|
||||
新用户发帖冷静期
|
||||
</h2>
|
||||
<p className="meta mt-1 text-[12.5px] leading-relaxed mb-3">
|
||||
注册后满该小时数才能发帖;设为 0 表示关闭。上限 168 小时(7 天)。
|
||||
</p>
|
||||
<AdminSettingRow
|
||||
title="新用户发帖冷静期"
|
||||
description="注册后满该小时数才能发帖;设为 0 表示关闭。上限 168 小时(7 天)。"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<input
|
||||
type="number"
|
||||
@@ -354,89 +315,52 @@ export default function ContentAdmin({
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</AdminSettingRow>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5 mb-3 flex-wrap" role="tablist" aria-label="内容类型">
|
||||
{(
|
||||
[
|
||||
{ key: "posts" as Kind, label: "帖子", icon: <FileText size={14} />, count: counts.posts_live },
|
||||
{
|
||||
key: "comments" as Kind,
|
||||
label: "评论",
|
||||
icon: <MessagesSquare size={14} />,
|
||||
count: counts.comments_live,
|
||||
},
|
||||
]
|
||||
).map((t) => {
|
||||
const active = kind === t.key;
|
||||
return (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
onClick={() => switchKind(t.key)}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-[13px] transition-colors"
|
||||
style={
|
||||
active
|
||||
? { background: "var(--nav-active-bg)", color: "var(--nav-active-ink)" }
|
||||
: { color: "var(--ink-2)", background: "var(--panel-2)" }
|
||||
}
|
||||
>
|
||||
{t.icon}
|
||||
{t.label}
|
||||
<span className="text-[11px] tabular-nums" style={{ color: active ? "var(--nav-active-ink)" : "var(--ink-3)" }}>
|
||||
{t.count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 mb-4 flex-wrap" role="tablist" aria-label="状态筛选">
|
||||
{STATUS_TABS.map((t) => {
|
||||
const active = status === t.key;
|
||||
const cnt = statusCount(t.key);
|
||||
return (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
onClick={() => switchStatus(t.key)}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[12.5px] transition-colors"
|
||||
style={
|
||||
active
|
||||
? { background: "var(--accent-soft)", color: "var(--accent)" }
|
||||
: { color: "var(--ink-3)", background: "var(--panel)" }
|
||||
}
|
||||
>
|
||||
{t.label}
|
||||
{cnt != null && (
|
||||
<span className="tabular-nums text-[11px] opacity-80">{cnt}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<form onSubmit={applySearch} className="panel p-3 mb-4 flex gap-2 items-center" role="search">
|
||||
<Search size={15} style={{ color: "var(--ink-3)" }} aria-hidden />
|
||||
<input
|
||||
className="field flex-1 !border-0 !shadow-none !bg-transparent py-1.5"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder={kind === "posts" ? "搜索帖子标题…" : "搜索评论正文…"}
|
||||
aria-label="搜索内容"
|
||||
/>
|
||||
<button type="submit" className="btn btn-line btn-sm">
|
||||
搜索
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-16 meta text-[13px]">
|
||||
<Loader2 size={16} className="animate-spin" /> 加载中…
|
||||
@@ -444,40 +368,43 @@ export default function ContentAdmin({
|
||||
) : error ? (
|
||||
<p className="alert-error m-4">{error}</p>
|
||||
) : kind === "posts" && posts.length === 0 ? (
|
||||
<p className="meta text-center py-16 text-[13px]">暂无帖子</p>
|
||||
<AdminEmpty icon={<FileText size={22} />} title="暂无帖子" />
|
||||
) : kind === "comments" && comments.length === 0 ? (
|
||||
<p className="meta text-center py-16 text-[13px]">暂无评论</p>
|
||||
<AdminEmpty icon={<MessagesSquare size={22} />} title="暂无评论" />
|
||||
) : kind === "posts" ? (
|
||||
posts.map((p) => {
|
||||
posts.map((p, idx) => {
|
||||
const key = `p-${p.id}`;
|
||||
const busy = actingKey === key;
|
||||
const chip = contentStatusChip(p.status, p.deleted);
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex items-start gap-3 px-4 sm:px-5 py-3.5"
|
||||
style={{ borderTop: "1px solid var(--line)" }}
|
||||
style={idx === 0 ? undefined : { borderTop: "1px solid var(--line)" }}
|
||||
>
|
||||
<Avatar name={p.user?.nickname || p.user?.username || "?"} src={p.user?.avatar || undefined} size={34} />
|
||||
<Avatar
|
||||
name={p.user?.nickname || p.user?.username || "?"}
|
||||
src={p.user?.avatar || undefined}
|
||||
size={34}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className="text-[11px] px-2 py-0.5 rounded-full"
|
||||
style={{
|
||||
background: p.deleted ? "var(--danger-soft)" : "var(--tint)",
|
||||
color: p.deleted ? "var(--danger)" : "var(--ink-3)",
|
||||
}}
|
||||
>
|
||||
{statusLabel(p.status, p.deleted)}
|
||||
</span>
|
||||
<AdminStatusChip tone={chip.tone}>{chip.label}</AdminStatusChip>
|
||||
<span className="meta text-[11.5px]">{p.board?.name}</span>
|
||||
<span className="meta text-[11.5px] tabular-nums">{formatRelative(p.created_at)}</span>
|
||||
<span className="meta text-[11.5px] tabular-nums">
|
||||
{formatRelative(p.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
{p.deleted ? (
|
||||
<p className="mt-1 text-[14px]" style={{ color: "var(--ink-2)" }}>
|
||||
{p.title}
|
||||
</p>
|
||||
) : (
|
||||
<Link href={`/post/${p.id}`} className="mt-1 block text-[14px] hover:underline" style={{ color: "var(--ink)" }}>
|
||||
<Link
|
||||
href={`/post/${p.id}`}
|
||||
className="mt-1 block text-[14px] hover:underline"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{p.title}
|
||||
</Link>
|
||||
)}
|
||||
@@ -488,20 +415,24 @@ export default function ContentAdmin({
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{p.deleted ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
<AdminIconButton
|
||||
tip="恢复"
|
||||
ariaLabel="恢复帖子"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
runAction(key, () => apiAdminRestorePost(p.id), "帖子已恢复")
|
||||
}
|
||||
>
|
||||
{busy ? <Loader2 size={14} className="animate-spin" /> : <RotateCcw size={14} />}
|
||||
恢复
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger btn-sm"
|
||||
{busy ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<RotateCcw size={14} />
|
||||
)}
|
||||
</AdminIconButton>
|
||||
<AdminIconButton
|
||||
tip="清除"
|
||||
ariaLabel="彻底删除帖子"
|
||||
tone="danger"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
askConfirm(
|
||||
@@ -516,47 +447,46 @@ export default function ContentAdmin({
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 size={14} /> 清除
|
||||
</button>
|
||||
<Trash2 size={14} />
|
||||
</AdminIconButton>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
<AdminIconButton
|
||||
tip="删除"
|
||||
ariaLabel="删除帖子"
|
||||
disabled={busy}
|
||||
onClick={() => setStaffDeletePost(p)}
|
||||
>
|
||||
<Trash2 size={14} /> 删除
|
||||
</button>
|
||||
<Trash2 size={14} />
|
||||
</AdminIconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
comments.map((c) => {
|
||||
comments.map((c, idx) => {
|
||||
const key = `c-${c.id}`;
|
||||
const busy = actingKey === key;
|
||||
const chip = contentStatusChip(c.status, c.deleted);
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
className="flex items-start gap-3 px-4 sm:px-5 py-3.5"
|
||||
style={{ borderTop: "1px solid var(--line)" }}
|
||||
style={idx === 0 ? undefined : { borderTop: "1px solid var(--line)" }}
|
||||
>
|
||||
<Avatar name={c.user?.nickname || c.user?.username || "?"} src={c.user?.avatar || undefined} size={34} />
|
||||
<Avatar
|
||||
name={c.user?.nickname || c.user?.username || "?"}
|
||||
src={c.user?.avatar || undefined}
|
||||
size={34}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className="text-[11px] px-2 py-0.5 rounded-full"
|
||||
style={{
|
||||
background: c.deleted ? "var(--danger-soft)" : "var(--tint)",
|
||||
color: c.deleted ? "var(--danger)" : "var(--ink-3)",
|
||||
}}
|
||||
>
|
||||
{statusLabel(c.status, c.deleted)}
|
||||
</span>
|
||||
<AdminStatusChip tone={chip.tone}>{chip.label}</AdminStatusChip>
|
||||
<span className="meta text-[11.5px]">{c.board?.name}</span>
|
||||
<span className="meta text-[11.5px] tabular-nums">{formatRelative(c.created_at)}</span>
|
||||
<span className="meta text-[11.5px] tabular-nums">
|
||||
{formatRelative(c.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[13.5px] leading-relaxed" style={{ color: "var(--ink)" }}>
|
||||
{excerpt(c.content)}
|
||||
@@ -566,36 +496,44 @@ export default function ContentAdmin({
|
||||
{c.deleted ? (
|
||||
c.post_title
|
||||
) : (
|
||||
<Link href={`/post/${c.post_id}`} className="hover:underline" style={{ color: "var(--accent)" }}>
|
||||
<Link
|
||||
href={`/post/${c.post_id}`}
|
||||
className="hover:underline"
|
||||
style={{ color: "var(--accent)" }}
|
||||
>
|
||||
{c.post_title || `帖子 #${c.post_id}`}
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
<AdminIconButton
|
||||
tip="历史"
|
||||
ariaLabel="查看编辑历史"
|
||||
onClick={() => setHistoryCommentId(c.id)}
|
||||
>
|
||||
<History size={14} /> 历史
|
||||
</button>
|
||||
<History size={14} />
|
||||
</AdminIconButton>
|
||||
{c.deleted ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
<AdminIconButton
|
||||
tip="恢复"
|
||||
ariaLabel="恢复评论"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
runAction(key, () => apiAdminRestoreComment(c.id), "评论已恢复")
|
||||
}
|
||||
>
|
||||
{busy ? <Loader2 size={14} className="animate-spin" /> : <RotateCcw size={14} />}
|
||||
恢复
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger btn-sm"
|
||||
{busy ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<RotateCcw size={14} />
|
||||
)}
|
||||
</AdminIconButton>
|
||||
<AdminIconButton
|
||||
tip="清除"
|
||||
ariaLabel="彻底删除评论"
|
||||
tone="danger"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
askConfirm(
|
||||
@@ -610,18 +548,18 @@ export default function ContentAdmin({
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 size={14} /> 清除
|
||||
</button>
|
||||
<Trash2 size={14} />
|
||||
</AdminIconButton>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
<AdminIconButton
|
||||
tip="删除"
|
||||
ariaLabel="删除评论"
|
||||
disabled={busy}
|
||||
onClick={() => setStaffDeleteComment(c)}
|
||||
>
|
||||
<Trash2 size={14} /> 删除
|
||||
</button>
|
||||
<Trash2 size={14} />
|
||||
</AdminIconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -629,40 +567,13 @@ export default function ContentAdmin({
|
||||
})
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div
|
||||
className="flex items-center justify-center gap-2 px-4 py-3"
|
||||
style={{ borderTop: "1px solid var(--line)" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={page <= 1 || loading}
|
||||
onClick={() => {
|
||||
const p = page - 1;
|
||||
if (kind === "posts") void loadPosts(p, status, appliedQ);
|
||||
else void loadComments(p, status, appliedQ);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={14} /> 上一页
|
||||
</button>
|
||||
<span className="meta text-[12.5px] tabular-nums">
|
||||
{page} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={page >= totalPages || loading}
|
||||
onClick={() => {
|
||||
const p = page + 1;
|
||||
if (kind === "posts") void loadPosts(p, status, appliedQ);
|
||||
else void loadComments(p, status, appliedQ);
|
||||
}}
|
||||
>
|
||||
下一页 <ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<AdminPager
|
||||
page={page}
|
||||
total={total}
|
||||
pageSize={PAGE_SIZE}
|
||||
loading={loading}
|
||||
onChange={goPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user