feat(admin): 管理后台 - 统一桌面端信息架构与视觉规范

This commit is contained in:
HANG LU
2026-08-30 05:05:46 +08:00
parent d89f170a08
commit 10cebf8c69
32 changed files with 17070 additions and 1860 deletions

View File

@@ -43,26 +43,27 @@ export function createUserColumns({
</div>
),
},
{ key: "email", title: "邮箱", dataIndex: "email", render: (email) => email || <span className="text-foreground/40">未填写</span> },
{ key: "email", title: "邮箱", dataIndex: "email", align: "center", render: (email) => email || <span className="text-foreground/40">未填写</span> },
{
key: "credits",
title: "当前积分",
dataIndex: "availableMicrocredits",
width: 130,
align: "right",
align: "center",
render: (value, user) => <span className="tabular-nums" title={`冻结积分:${formatCredits(user.reservedMicrocredits)}`}>{formatCredits(value)}</span>,
},
{ key: "role", title: "角色", dataIndex: "role", width: 110, render: (role) => <AdminStatusBadge label={role === "admin" ? "管理员" : "普通用户"} tone={role === "admin" ? "info" : "neutral"} /> },
{ key: "status", title: "状态", dataIndex: "status", width: 110, render: (status) => <AdminStatusBadge label={status === "active" ? "已启用" : "已停用"} tone={status === "active" ? "success" : "neutral"} /> },
{ key: "createdAt", title: "注册时间", dataIndex: "createdAt", width: 180, render: formatTime },
{ key: "role", title: "角色", dataIndex: "role", width: 110, align: "center", render: (role) => <AdminStatusBadge label={role === "admin" ? "管理员" : "普通用户"} tone={role === "admin" ? "info" : "neutral"} /> },
{ key: "status", title: "状态", dataIndex: "status", width: 110, align: "center", render: (status) => <AdminStatusBadge label={status === "active" ? "已启用" : "已停用"} tone={status === "active" ? "success" : "neutral"} /> },
{ key: "createdAt", title: "注册时间", dataIndex: "createdAt", width: 180, align: "center", render: formatTime },
{
key: "actions",
title: "操作",
width: 180,
align: "right",
width: 280,
align: "center",
render: (_, user) => (
<AdminRowActions
primary={{ label: "详情", icon: <Eye className="size-3.5" />, onClick: () => onView(user) }}
visibleActionCount={2}
actions={[
{ key: "edit", label: "编辑用户", icon: <Pencil className="size-3.5" />, onClick: () => onEdit(user) },
{

View File

@@ -74,9 +74,9 @@ export function AdminUserEditDrawer({
<Drawer
title={user ? `编辑用户 · ${user.displayName || user.username}` : "编辑用户"}
open={Boolean(user)}
width="min(520px, 100vw)"
size="min(520px, 100vw)"
onClose={close}
maskClosable={!saving}
mask={{ closable: !saving }}
destroyOnHidden
extra={<Button type="primary" loading={saving} onClick={() => void save()}>保存</Button>}
>
@@ -172,9 +172,9 @@ export function AdminUserCreateDrawer({
<Drawer
title={"\u6dfb\u52a0\u7528\u6237"}
open={open}
width="min(520px, 100vw)"
size="min(520px, 100vw)"
onClose={close}
maskClosable={!saving}
mask={{ closable: !saving }}
destroyOnHidden
extra={<Button type="primary" loading={saving} onClick={() => void save()}>{"\u4fdd\u5b58"}</Button>}
>

View File

@@ -1,12 +1,12 @@
import { App, Button, Checkbox, Dropdown, Input } from "antd";
import { Ban, ChevronDown, Search, Settings2, UserPlus } from "lucide-react";
import { App, Button, Checkbox, Dropdown, Input, Select } from "antd";
import { Ban, Search, Settings2, UserPlus } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { PaginationBar } from "@/components/layout/workspace-page";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { bulkDisableAdminUsers, deleteAdminUser, listAdminUsers, updateAdminUser, type AdminUser, type LocalUser } from "@/services/api/auth";
import { useUserStore } from "@/stores/use-user-store";
import { AdminBatchBar, AdminDataTable, AdminFilterChip, AdminTableEmpty } from "../components/admin-ui";
import { AdminBatchBar, AdminDataTable, AdminTableEmpty } from "../components/admin-ui";
import { useTableUrlState } from "../lib/use-table-url-state";
import { AdminUserDetailDrawer } from "../components/admin-user-detail-drawer";
import { createUserColumns, userColumnOptions, type UserColumnKey } from "./users-columns";
@@ -144,30 +144,26 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
className="app-list-search"
prefix={<Search className="size-4 text-foreground/40" />}
value={state.filter}
aria-label="搜索用户"
placeholder="搜索用户名、名称或邮箱"
onChange={(event) => update({ filter: event.target.value, page: 1 }, true)}
/>
</>
}
toolbarActiveFilters={(
<>
{state.filter ? <AdminFilterChip label={`搜索:${state.filter}`} onRemove={() => update({ filter: "", page: 1 })} /> : null}
{state.role !== "all" ? <AdminFilterChip label={`角色:${state.role === "admin" ? "管理员" : "普通用户"}`} onRemove={() => update({ role: "all", page: 1 })} /> : null}
{state.status !== "all" ? <AdminFilterChip label={`状态:${state.status === "active" ? "已启用" : "已停用"}`} onRemove={() => update({ status: "all", page: 1 })} /> : null}
</>
)}
toolbarActive={hasFilters}
onReset={resetFilters}
toolbarFilters={
<>
<FilterMenu
label="角色"
<Select
aria-label="筛选用户角色"
className="w-32"
value={state.role}
options={[{ value: "all", label: "全部角色" }, { value: "admin", label: "管理员" }, { value: "user", label: "普通用户" }]}
onChange={(role) => update({ role, page: 1 })}
/>
<FilterMenu
label="状态"
<Select
aria-label="筛选用户状态"
className="w-32"
value={state.status}
options={[{ value: "all", label: "全部状态" }, { value: "active", label: "已启用" }, { value: "disabled", label: "已停用" }]}
onChange={(status) => update({ status, page: 1 })}
@@ -179,7 +175,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
<Button icon={<UserPlus className="size-4" />} onClick={() => { setEditingUser(null); setCreateUserOpen(true); }}>{"\u6dfb\u52a0\u7528\u6237"}</Button>
<Dropdown
trigger={["click"]}
dropdownRender={() => (
popupRender={() => (
<div className="w-48 rounded-md border border-border bg-popover p-2 shadow-lg">
<div className="px-2 pb-2 text-xs font-medium text-foreground/55">显示列</div>
<div className="space-y-0.5">
@@ -234,20 +230,3 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
</>
);
}
function FilterMenu({ label, value, options, onChange }: { label: string; value: string; options: Array<{ value: string; label: string }>; onChange: (value: string) => void }) {
const selected = options.find((option) => option.value === value)?.label || label;
return (
<Dropdown
trigger={["click"]}
menu={{
selectable: true,
selectedKeys: [value],
items: options.map((option) => ({ key: option.value, label: option.label })),
onClick: ({ key }) => onChange(key),
}}
>
<Button>{value === "all" ? label : selected}<ChevronDown className="ml-1 size-3.5" /></Button>
</Dropdown>
);
}