From 36f4474ad63da639e3d629302695746b23566bde Mon Sep 17 00:00:00 2001 From: yuqei Date: Mon, 1 Jun 2026 23:35:53 +0800 Subject: [PATCH] feat: redesign users page with left-tree right-detail layout - Left sidebar: tree menu showing all users grouped by role (admin/legal/business) and status (active/disabled) - Right panel: selected user's detail with role, status toggle, and account info - Create/edit moved to Drawer Co-Authored-By: Claude Opus 4.7 --- frontend/src/app/users/page.tsx | 753 ++++++++++++++++++++++---------- 1 file changed, 527 insertions(+), 226 deletions(-) diff --git a/frontend/src/app/users/page.tsx b/frontend/src/app/users/page.tsx index b03f7407..a8dbb097 100644 --- a/frontend/src/app/users/page.tsx +++ b/frontend/src/app/users/page.tsx @@ -1,7 +1,28 @@ -"use client"; +"use client"; -import { Alert, Button, Card, Empty, Form, Input, Select, Space, Switch, Table, Tag, Typography, message } from "antd"; -import type { ColumnsType, TablePaginationConfig } from "antd/es/table"; +import { + CaretDownOutlined, + CaretRightOutlined, + PlusOutlined, + SearchOutlined, + UserOutlined, +} from "@ant-design/icons"; +import { + Button, + Drawer, + Empty, + Form, + Input, + message, + Select, + Space, + Spin, + Switch, + Tag, + Tree, + Typography, +} from "antd"; +import type { DataNode } from "antd/es/tree"; import { useCallback, useEffect, useMemo, useState } from "react"; import { formatRoleLabel } from "@/lib/roles"; @@ -19,89 +40,184 @@ type UserFormValues = { status: string; }; -const roleOptions = [ - { value: "admin", label: "管理员" }, - { value: "legal", label: "法务用户" }, - { value: "business", label: "业务用户" }, -]; +const roleLabelMap: Record = { + admin: "管理员", + legal: "法务用户", + business: "业务用户", + member: "业务用户", +}; -const statusOptions = [ - { value: "active", label: "启用" }, - { value: "disabled", label: "停用" }, -]; +const statusLabelMap: Record = { + active: "启用", + disabled: "停用", +}; function formatDateTime(value: string) { return new Date(value).toLocaleString("zh-CN", { hour12: false }); } +const ALL_KEY = "__all__"; + export default function UsersPage() { const [messageApi, contextHolder] = message.useMessage(); const currentUser = useAuthStore((state) => state.currentUser); const [form] = Form.useForm(); + const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [statusChangingId, setStatusChangingId] = useState(null); const [items, setItems] = useState([]); const [editingId, setEditingId] = useState(null); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [total, setTotal] = useState(0); + + const [selectedId, setSelectedId] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [expandedKeys, setExpandedKeys] = useState([ALL_KEY]); + const [drawerOpen, setDrawerOpen] = useState(false); const editingUser = useMemo(() => items.find((item) => item.id === editingId) ?? null, [editingId, items]); + const selectedUser = useMemo(() => items.find((item) => item.id === selectedId) ?? null, [items, selectedId]); const resetFormDefaults = useCallback(() => { form.resetFields(); - form.setFieldsValue({ role: "business", status: "active" }); + form.setFieldsValue({ role: "business", status: "active", password: undefined }); }, [form]); - const loadUsers = useCallback( - async (nextPage = page, nextPageSize = pageSize) => { - if (currentUser?.role !== "admin") { - setLoading(false); - return; - } - - try { - const result = await listUsers(nextPage, nextPageSize); - if (result.code !== 0) { - messageApi.error(result.message || "获取用户列表失败"); - return; - } - setItems(result.data.items); - setPage(result.data.page); - setPageSize(result.data.page_size); - setTotal(result.data.total); - } catch { - messageApi.error("用户列表加载失败,请稍后重试"); - } finally { - setLoading(false); - } - }, - [currentUser?.role, messageApi, page, pageSize], - ); - - useEffect(() => { - const timer = window.setTimeout(() => { - void loadUsers(); - }, 0); - - return () => window.clearTimeout(timer); - }, [loadUsers]); - - useEffect(() => { - if (!editingUser) { - resetFormDefaults(); + const loadUsers = useCallback(async () => { + if (currentUser?.role !== "admin") { + setLoading(false); return; } - form.setFieldsValue({ - username: editingUser.username, - display_name: editingUser.display_name, - password: "", - role: editingUser.role === "member" ? "business" : editingUser.role, - status: editingUser.status, + setLoading(true); + try { + const result = await listUsers(1, 1000); + if (result.code !== 0) { + messageApi.error(result.message || "获取用户列表失败"); + return; + } + setItems(result.data.items); + if (!selectedId && result.data.items.length > 0) { + setSelectedId(result.data.items[0].id); + } + } catch { + messageApi.error("用户列表加载失败,请稍后重试"); + } finally { + setLoading(false); + } + }, [currentUser?.role, messageApi, selectedId]); + + useEffect(() => { + void loadUsers(); + }, [loadUsers]); + + const filteredItems = useMemo(() => { + if (!searchQuery.trim()) return items; + const q = searchQuery.toLowerCase(); + return items.filter( + (item) => + (item.display_name ?? "").toLowerCase().includes(q) || + (item.username ?? "").toLowerCase().includes(q) || + (item.role ?? "").toLowerCase().includes(q), + ); + }, [items, searchQuery]); + + const treeData = useMemo((): DataNode[] => { + const roles = Array.from(new Set(items.map((i) => i.role === "member" ? "business" : i.role).filter(Boolean))); + const statuses = Array.from(new Set(items.map((i) => i.status).filter(Boolean))); + + const roleChildren: DataNode[] = roles.map((role) => { + const usersInRole = filteredItems.filter((i) => (i.role === "member" ? "business" : i.role) === role); + return { + key: `role:${role}`, + title: ( + + {roleLabelMap[role] ?? role} + {usersInRole.length} + + ), + selectable: false, + children: usersInRole.map((u) => ({ + key: u.id, + title: ( +
+ + {u.display_name || u.username} + + {statusLabelMap[u.status]?.slice(0, 1) || u.status} + +
+ ), + isLeaf: true, + })), + }; }); - }, [editingUser, form, resetFormDefaults]); + + const statusChildren: DataNode[] = statuses.map((status) => { + const usersInStatus = filteredItems.filter((i) => i.status === status); + return { + key: `status:${status}`, + title: ( + + {statusLabelMap[status] || status} + {usersInStatus.length} + + ), + selectable: false, + children: usersInStatus.map((u) => ({ + key: u.id, + title: ( +
+ + {u.display_name || u.username} + + {roleLabelMap[u.role === "member" ? "business" : u.role]?.slice(0, 2) || u.role} + +
+ ), + isLeaf: true, + })), + }; + }); + + return [ + { + key: ALL_KEY, + title: ( + + 全部用户 + {filteredItems.length} + + ), + selectable: false, + children: [ + { key: "group-role", title: 按角色, selectable: false, children: roleChildren }, + { key: "group-status", title: 按状态, selectable: false, children: statusChildren }, + ], + }, + ]; + }, [filteredItems]); + + const handleTreeSelect = (_selectedKeys: React.Key[], info: { node: DataNode }) => { + const key = info.node.key as string; + if (key.startsWith("group-") || key === ALL_KEY) return; + setSelectedId(key); + }; + + const handleOpenDrawer = (user?: UserItem) => { + if (user) { + setEditingId(user.id); + } else { + setEditingId(null); + resetFormDefaults(); + } + setDrawerOpen(true); + }; + + const handleCloseDrawer = () => { + setDrawerOpen(false); + setEditingId(null); + resetFormDefaults(); + }; const handleSubmit = async (values: UserFormValues) => { setSubmitting(true); @@ -129,9 +245,8 @@ export default function UsersPage() { } messageApi.success(editingUser ? "用户已更新" : "用户已创建"); - setEditingId(null); - resetFormDefaults(); - await loadUsers(page, pageSize); + handleCloseDrawer(); + await loadUsers(); } catch { messageApi.error("保存用户失败,请稍后重试"); } finally { @@ -155,10 +270,7 @@ export default function UsersPage() { } messageApi.success(checked ? "用户已启用" : "用户已停用"); - await loadUsers(page, pageSize); - if (editingId === item.id) { - setEditingId(item.id); - } + await loadUsers(); } catch { messageApi.error("更新用户状态失败,请稍后重试"); } finally { @@ -166,175 +278,364 @@ export default function UsersPage() { } }; - const handleTableChange = (pagination: TablePaginationConfig) => { - const nextPage = pagination.current ?? 1; - const nextPageSize = pagination.pageSize ?? pageSize; - setLoading(true); - void loadUsers(nextPage, nextPageSize); - }; - - const columns: ColumnsType = [ - { - title: "姓名", - dataIndex: "display_name", - key: "display_name", - width: 140, - }, - { - title: "账号", - dataIndex: "username", - key: "username", - width: 220, - }, - { - title: "角色", - dataIndex: "role", - key: "role", - width: 140, - render: (value: string) => ( - {formatRoleLabel(value)} - ), - }, - { - title: "状态", - dataIndex: "status", - key: "status", - width: 180, - render: (value: string, item) => ( - - {value === "active" ? "启用" : "停用"} - void handleToggleStatus(item, checked)} - /> - - ), - }, - { - title: "创建时间", - dataIndex: "created_at", - key: "created_at", - width: 180, - render: (value: string) => formatDateTime(value), - }, - { - title: "操作", - key: "actions", - width: 120, - render: (_, item) => ( - - ), - }, - ]; - if (currentUser?.role !== "admin") { return ( -
+
{contextHolder} - - - +
+ 仅管理员可管理用户。 +
); } return ( -
+
{contextHolder} - -
-
- User Management - 管理试点成员账号、角色和启停状态。 - 让管理员可以把同一套试点环境里的法务用户、业务用户和管理员账号明确分开。 -
- - Pilot Roles - - 最小角色集 - -
    -
  • 管理员:负责模板、规则、知识库、试点申请和治理后台。
  • -
  • 法务用户:优先进入合同审查、审查记录和法律问答链路。
  • -
  • 业务用户:优先进入合同生成、审查结果跟进和试点使用链路。
  • -
-
-
- -
- -
- - - - - - - - - - - - - - - {editingUser ? ( - - ) : null} - -
-
- - - }} - pagination={{ current: page, pageSize, total, showSizeChanger: true }} - onChange={handleTableChange} - scroll={{ x: 900 }} +
+ {/* 左侧树形菜单 */} +
+
+ {loading ? ( +
+ ) : ( + expanded ? : } + treeData={treeData} + expandedKeys={expandedKeys} + selectedKeys={selectedId ? [selectedId] : []} + onExpand={setExpandedKeys} + onSelect={handleTreeSelect} + blockNode + /> + )} +
+
+ +
+ + + {/* 右侧用户详情 */} +
+ {loading ? ( +
+ + 加载中... +
+ ) : selectedUser ? ( +
+ {/* 用户头部 */} +
+
+
+ + +
+ {selectedUser.display_name} + {selectedUser.username} +
+
+
+ + void handleToggleStatus(selectedUser, checked)} + checkedChildren="启用" + unCheckedChildren="停用" + /> + + +
+
+ + + {formatRoleLabel(selectedUser.role)} + + + {selectedUser.status === "active" ? "启用中" : "已停用"} + + + 创建时间:{formatDateTime(selectedUser.created_at)} +
+
+ + {/* 用户信息 */} +
+
+ 账号信息 +
+
+
+ 登录账号 + {selectedUser.username} +
+
+ 显示名称 + {selectedUser.display_name} +
+
+ 角色 + {formatRoleLabel(selectedUser.role)} +
+
+ 状态 + {selectedUser.status === "active" ? "启用" : "停用"} +
+
+ 创建时间 + {formatDateTime(selectedUser.created_at)} +
+
+
+
+ ) : ( +
+ + + +
+ )} +
+ + + {/* 新建/编辑用户抽屉 */} + +
+ + + + + + + + + + + + + + + + + +
+ + ); }