v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* # 路由 /admin/users 用户管理(系统设置,只读)
|
||||
* 用户列表:分页 + 关键字搜索;角色用 Tag 展示;不做编辑操作。
|
||||
*/
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Card, Input, Table, Tag } from "antd";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { useState } from "react";
|
||||
import { adminListUsers } 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<string, unknown>): AdminSearch => {
|
||||
// 只保留显式传入的参数,缺省时不回写 URL(避免 307 归一化重定向)
|
||||
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;
|
||||
return out;
|
||||
},
|
||||
loaderDeps: ({ search }) => ({ search }),
|
||||
loader: ({ deps }) => guardAdmin(() => adminListUsers({ data: { page: deps.search.page, q: deps.search.q || undefined } })),
|
||||
component: UsersPage,
|
||||
});
|
||||
|
||||
/** # 成员变量 角色中文标签与配色 **/
|
||||
const ROLE_LABEL: Record<string, string> = { admin: "管理员", editor: "编辑", manager: "经理", customer: "客户" };
|
||||
const ROLE_COLOR: Record<string, string> = { admin: "blue", editor: "cyan", manager: "geekblue", customer: "default" };
|
||||
|
||||
/** # 函数 用户列表页面组件 */
|
||||
function UsersPage() {
|
||||
const data = Route.useLoaderData();
|
||||
const search = Route.useSearch();
|
||||
const navigate = Route.useNavigate();
|
||||
const [q, setQ] = useState(search.q ?? "");
|
||||
|
||||
if (!data) return <Forbidden />;
|
||||
|
||||
return (
|
||||
<Card size="small">
|
||||
<PageHeader
|
||||
title="用户管理"
|
||||
extra={
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="搜索邮箱 / 显示名"
|
||||
prefix={<SearchOutlined aria-hidden />}
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onPressEnter={() => void navigate({ search: (prev) => ({ ...prev, q, page: 1 }) })}
|
||||
style={{ width: 240 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={data.items}
|
||||
locale={{ emptyText: <AdminEmpty /> }}
|
||||
pagination={tablePagination(data.total, data.page, data.pageSize, (p) =>
|
||||
void navigate({ search: (prev) => ({ ...prev, page: p }) }),
|
||||
)}
|
||||
columns={[
|
||||
{ title: "ID", dataIndex: "id", width: 80 },
|
||||
{ title: "邮箱", dataIndex: "email", width: 260, ellipsis: true },
|
||||
{ title: "用户名", dataIndex: "username", width: 160, ellipsis: true },
|
||||
{ title: "显示名", dataIndex: "displayName", width: 180, ellipsis: true },
|
||||
{
|
||||
title: "角色",
|
||||
dataIndex: "role",
|
||||
width: 110,
|
||||
render: (v: string) => <Tag color={ROLE_COLOR[v] ?? "default"}>{ROLE_LABEL[v] ?? v}</Tag>,
|
||||
},
|
||||
{ title: "注册时间", dataIndex: "createdAt", width: 160, render: (v: string) => fmtDateTime(v) },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user