对齐用户管理页面布局样式

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-02 23:14:39 +08:00
parent 58a20f25cd
commit 456d7da34d
2 changed files with 92 additions and 47 deletions
+25
View File
@@ -215,3 +215,28 @@
- 风险与影响:
- 影响范围仅 `syslog` 页面前端展示层。
- 接口请求、权限判断、筛选参数、分页逻辑保持不变。
## Work Log - 用户管理页面布局样式对齐菜单管理(2026-05-02)
- 背景:
- Issue `FL-167` 要求“参考菜单管理页面,调整用户管理页面样式和布局”。
- `web/src/app/admin/users/page.tsx` 原为“用户检索卡 + 用户列表卡”双卡结构,和菜单管理页不一致。
- 本次改动(最小闭环):
- 文件:`web/src/app/admin/users/page.tsx`
- 将用户管理主内容收敛为单卡布局(标题“用户列表”):
- 将筛选区合并进列表卡顶部,使用 `Form layout="inline"`
- 筛选项统一为“关键词 + 状态 + 搜索 + 重置筛选”。
- 统一反馈样式:
- 错误提示改为 closable `Alert`,并使用 `pre` 保留换行。
- 成功提示改为 closable `Alert`
- 空状态文案改为“未找到符合筛选条件的用户。”。
- 统一加载与权限态样式:
- 加载态调整为 `Spin tip="用户数据加载中..."` + `min-h-[240px]`
- 未登录/无权限态改为 `p + Link` 样式,和菜单管理页一致。
- 分页与间距细节对齐:
- 增加 `pageSizeOptions: [10, 20, 50, 100]`
- 表格增加 `className="mt-4"`,统一筛选区与表格间距。
- 风险与影响:
- 影响面仅 `web/src/app/admin/users/page.tsx` 前端展示层,不涉及后端接口、权限和数据结构。
+67 -47
View File
@@ -553,11 +553,8 @@ export default function AdminUsersPage() {
if (initializing || usersQuery.isLoading || rolesQuery.isLoading) {
return (
<div className="flex min-h-[40vh] items-center justify-center">
<Space direction="vertical" align="center" size={12}>
<Spin />
<Typography.Text type="secondary">...</Typography.Text>
</Space>
<div className="flex min-h-[240px] items-center justify-center">
<Spin tip="用户数据加载中..." />
</div>
);
}
@@ -565,10 +562,13 @@ export default function AdminUsersPage() {
if (!user) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col justify-center gap-4 px-6 py-20">
<Typography.Text type="secondary">访</Typography.Text>
<Button type="default" className="w-fit">
<Link href="/"></Link>
</Button>
<p className="text-sm text-[var(--gray-11)]">访</p>
<Link
href="/"
className="inline-flex w-fit items-center justify-center rounded-md border border-[var(--gray-6)] bg-[var(--gray-a2)] px-4 py-2 text-sm font-medium text-[var(--gray-12)] transition hover:bg-[var(--gray-a3)]"
>
</Link>
</main>
);
}
@@ -576,48 +576,30 @@ export default function AdminUsersPage() {
if (!canManage) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col justify-center gap-4 px-6 py-20">
<Typography.Text type="secondary">访 `user.manage`</Typography.Text>
<Button type="default" className="w-fit">
<Link href="/"></Link>
</Button>
<p className="text-sm text-[var(--gray-11)]">访 `user.manage`</p>
<Link
href="/"
className="inline-flex w-fit items-center justify-center rounded-md border border-[var(--gray-6)] bg-[var(--gray-a2)] px-4 py-2 text-sm font-medium text-[var(--gray-12)] transition hover:bg-[var(--gray-a3)]"
>
</Link>
</main>
);
}
return (
<div className="space-y-6">
{anyError && <Alert type="error" message="操作失败" description={anyError} showIcon />}
{success && <Alert type="success" message={success} showIcon />}
<AntCard
title="用户检索"
>
<Space wrap>
<Input
allowClear
placeholder="按用户ID/邮箱/用户名搜索"
value={keywordInput}
onChange={(event) => setKeywordInput(event.target.value)}
onPressEnter={handleSearch}
style={{ width: 320 }}
/>
<Select<"all" | "active" | "disabled">
value={statusFilter}
style={{ width: 160 }}
options={[
{ label: "全部状态", value: "all" },
{ label: "启用", value: "active" },
{ label: "禁用", value: "disabled" },
]}
onChange={(value) => {
setStatusFilter(value);
setPagination((prev) => ({ ...prev, current: 1 }));
}}
/>
<Button type="primary" onClick={handleSearch}></Button>
<Button onClick={handleResetSearch}></Button>
</Space>
</AntCard>
{anyError && (
<Alert
type="error"
showIcon
closable
message="操作失败"
description={<pre className="mb-0 whitespace-pre-wrap break-words">{anyError}</pre>}
onClose={() => setError("")}
/>
)}
{success && <Alert type="success" showIcon closable message={success} onClose={() => setSuccess("")} />}
<AntCard
title="用户列表"
@@ -631,7 +613,45 @@ export default function AdminUsersPage() {
</Space>
)}
>
<Form layout="inline" style={{ rowGap: 12 }}>
<Form.Item label="关键词" className="min-w-[240px]">
<Input
allowClear
placeholder="按用户 ID/邮箱/用户名搜索"
value={keywordInput}
onChange={(event) => setKeywordInput(event.target.value)}
onPressEnter={handleSearch}
/>
</Form.Item>
<Form.Item label="状态" className="min-w-[170px]">
<Select<"all" | "active" | "disabled">
value={statusFilter}
options={[
{ value: "all", label: "全部" },
{ value: "active", label: "已启用" },
{ value: "disabled", label: "已禁用" },
]}
onChange={(value) => {
setStatusFilter(value);
setPagination((prev) => ({ ...prev, current: 1 }));
}}
/>
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleSearch}>
</Button>
</Form.Item>
<Form.Item>
<Button onClick={handleResetSearch}></Button>
</Form.Item>
</Form>
<Table<UserPublic>
className="mt-4"
rowKey="id"
dataSource={users}
columns={columns}
@@ -640,18 +660,18 @@ export default function AdminUsersPage() {
pageSize: pagination.pageSize,
total: usersQuery.data?.total ?? 0,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setPagination({ current: page, pageSize });
},
}}
size="middle"
scroll={{ x: 1500 }}
locale={{
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无用户数据"
description="未找到符合筛选条件的用户。"
/>
),
}}