/** * # 路由 /admin/users 用户管理 * 用户列表(分页 + 搜索);admin 可新建用户、改角色、重置密码。 * 服务端有防锁死保护:最后一个 admin 不可降级。 */ import { createFileRoute, useRouter } from "@tanstack/react-router"; import { App, Button, Card, Form, Input, Modal, Select, Space, Table, Tabs, Tag } from "antd"; import { KeyRound, Search, UserPlus } from "lucide-react"; import { getCurrentUser } from "~/server/auth"; import { useState } from "react"; import { adminCreateUser, adminListUsers, adminResetUserPassword, adminUpdateUserRole } from "~/server/admin"; import { AdminEmpty, Forbidden, PageHeader, fmtDateTime, guardAdmin, tablePagination, type AdminSearch } from "~/components/admin/common"; export const Route = createFileRoute("/admin/users")({ validateSearch: (search: Record): AdminSearch => { const out: AdminSearch = {}; const page = Math.floor(Number(search.page)) || 0; if (page > 1) out.page = page; if (typeof search.q === "string" && search.q) out.q = search.q; // 用户类型 Tab:backend 后台用户 / customer 普通用户 if (search.kind === "backend" || search.kind === "customer") out.kind = search.kind; return out; }, loaderDeps: ({ search }) => ({ search }), loader: ({ deps }) => guardAdmin(async () => { const [user, me] = await Promise.all([ adminListUsers({ data: { page: deps.search.page, q: deps.search.q || undefined, kind: deps.search.kind } }), getCurrentUser(), ]); return { users: user, me }; }), component: UsersPage, }); /** # 成员变量 角色中文标签与配色 **/ const ROLE_LABEL: Record = { admin: "管理员", editor: "编辑", manager: "经理", customer: "客户" }; const ROLE_COLOR: Record = { admin: "blue", editor: "cyan", manager: "geekblue", customer: "default" }; const ROLE_OPTIONS = (["admin", "editor", "manager", "customer"] as const).map((r) => ({ value: r, label: ROLE_LABEL[r] ?? r })); /** # 函数 用户列表页面组件 */ function UsersPage() { const data = Route.useLoaderData(); const search = Route.useSearch(); const navigate = Route.useNavigate(); const router = useRouter(); const { message, modal } = App.useApp(); const [q, setQ] = useState(search.q ?? ""); /** # 成员变量 新建用户弹窗开关 **/ const [createOpen, setCreateOpen] = useState(false); const [createForm] = Form.useForm(); const isAdmin = data?.me?.role === "admin"; if (!data) return ; /** # 函数 修改用户角色(服务端校验最后一个 admin 不可降级) */ async function handleRoleChange(id: number, role: string) { try { await adminUpdateUserRole({ data: { id, role } }); message.success("角色已更新"); await router.invalidate(); } catch (e) { message.error(e instanceof Error ? e.message : "更新失败"); } } /** # 函数 重置密码弹窗(admin 专用) */ function openResetPassword(id: number, email: string) { let pwd = ""; modal.confirm({ title: `重置密码:${email}`, content: (pwd = e.target.value)} autoFocus />, okText: "重置", cancelText: "取消", onOk: async () => { try { await adminResetUserPassword({ data: { id, password: pwd } }); message.success("密码已重置"); } catch (e) { message.error(e instanceof Error ? e.message : "重置失败"); } }, }); } /** # 函数 新建用户提交 */ async function handleCreate(values: { email: string; username?: string; displayName?: string; password: string; role: string }) { try { await adminCreateUser({ data: values }); message.success("用户已创建"); setCreateOpen(false); createForm.resetFields(); await router.invalidate(); } catch (e) { message.error(e instanceof Error ? e.message : "创建失败"); } } return ( } value={q} onChange={(e) => setQ(e.target.value)} onPressEnter={() => void navigate({ search: (prev) => ({ ...prev, q, page: 1 }) })} style={{ width: 240 }} /> {isAdmin ? ( ) : null} } /> {/* 用户类型分层:后台用户(admin/editor/manager)可登录后台;普通用户(customer)仅商城账号 */} void navigate({ search: (prev) => ({ ...prev, kind: key || undefined, page: 1 }) })} items={[ { key: "", label: "全部" }, { key: "backend", label: "后台用户" }, { key: "customer", label: "普通用户" }, ]} /> }} pagination={tablePagination(data.users.total, data.users.page, data.users.pageSize, (p) => void navigate({ search: (prev) => ({ ...prev, page: p }) }), )} columns={[ { title: "ID", dataIndex: "id", width: 80 }, { title: "邮箱", dataIndex: "email", width: 240, ellipsis: true }, { title: "显示名", dataIndex: "displayName", width: 170, ellipsis: true }, { title: "角色", dataIndex: "role", width: 170, render: (v: string, r) => isAdmin ? (