import { useCallback, useEffect, useMemo, useState } from "react"; import { Plus, Search } from "lucide-react"; import { ApiRequestError, type Employee, type Role, type Workspace } from "../../services/api"; import { useApi, useAuth } from "../../context/AuthContext"; import { ConfirmDialog } from "~/components/common/ConfirmDialog"; import { Button } from "~/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable"; import { AdminPagination } from "./AdminPagination"; import { UserFormDialog, type UserFormState } from "./UserFormDialog"; import { ResetPasswordDialog } from "./ResetPasswordDialog"; import { useCursorPage } from "./useCursorPage"; import { adminEmptyClass, adminPageClass, adminReadonlyClass, adminToolbarClass, adminToolbarInputClass, adminToolbarSearchClass, employeeActionsClass, employeeAvatarClass, employeeNameClass, rolePillClass, rowButtonClass, rowDangerButtonClass, statusPillClass, } from "./adminUi"; import { primaryGradientButtonClass } from "~/components/common/buttonClasses"; import { useDebouncedValue } from "./useDebouncedValue"; const EMPTY_FORM: UserFormState = { username: "", display_name: "", email: "", role_code: "developer", password: "", status: "active", }; export function UserManagementPage({ onNotify, onConnectionChange, }: { onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void; onConnectionChange: (online: boolean) => void; }) { const api = useApi(); const { user } = useAuth(); const [roles, setRoles] = useState([]); const [saving, setSaving] = useState(false); const [editing, setEditing] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(EMPTY_FORM); const [userSearchTerm, setUserSearchTerm] = useState(""); const [deleteTarget, setDeleteTarget] = useState(null); const [resetTarget, setResetTarget] = useState(null); const [resetSaving, setResetSaving] = useState(false); const [createProjects, setCreateProjects] = useState([]); const [createProjectsLoading, setCreateProjectsLoading] = useState(false); const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState([]); const debouncedSearch = useDebouncedValue(userSearchTerm, 300); const canManage = user?.role_code === "admin"; const fetchEmployees = useCallback( async (input: { limit: number; cursor: string | null; q: string }) => { try { const page = await api.listPlatformEmployees(input); onConnectionChange(true); return page; } catch (error) { onConnectionChange(false); onNotify({ tone: "error", message: error instanceof Error ? error.message : "用户列表加载失败", }); throw error; } }, [api, onConnectionChange, onNotify], ); const { items: employees, setItems: setEmployees, page, loading, meta, totalPages, goNext, goPrev, goToPage, canGoToPage, reload, refreshFromStart, } = useCursorPage(fetchEmployees, debouncedSearch); useEffect(() => { void api.listPlatformRoles().then(setRoles).catch(() => { /* roles only needed for dialog */ }); }, [api]); const createRoleOptions = useMemo( () => roles.filter((role) => role.role_code !== "admin"), [roles], ); const loadCreateProjects = useCallback(async (): Promise => { setCreateProjectsLoading(true); try { const page = await api.listWorkspaces({ limit: 200, cursor: null, q: "" }); setCreateProjects(page.items.filter((item) => item.status === "active")); } catch { setCreateProjects([]); onNotify({ tone: "error", message: "项目列表加载失败" }); } finally { setCreateProjectsLoading(false); } }, [api, onNotify]); const openCreate = (): void => { setEditing(null); setForm(EMPTY_FORM); setSelectedWorkspaceIds([]); setDialogOpen(true); void loadCreateProjects(); }; const openEdit = (employee: Employee): void => { setEditing(employee); setForm({ username: employee.username, display_name: employee.display_name, email: employee.email ?? "", role_code: employee.role_code, password: "", status: employee.status, }); setSelectedWorkspaceIds([]); setDialogOpen(true); }; const toggleCreateWorkspace = (workspaceId: string): void => { setSelectedWorkspaceIds((current) => current.includes(workspaceId) ? current.filter((id) => id !== workspaceId) : [...current, workspaceId], ); }; const submit = async (event: React.FormEvent): Promise => { event.preventDefault(); if (!form.display_name.trim() || (!editing && !form.username.trim())) return; if (!editing && !form.password.trim()) { onNotify({ tone: "error", message: "请输入密码" }); return; } if (editing) { const editingSelf = editing.user_id === user?.user_id; const targetIsAdmin = editing.role_code === "admin"; if (editingSelf && form.status !== editing.status) { onNotify({ tone: "error", message: "不能停用当前登录账号" }); return; } if (targetIsAdmin && form.status !== editing.status) { onNotify({ tone: "error", message: "不能停用或锁定管理员账号" }); return; } if (editingSelf && form.role_code !== editing.role_code) { onNotify({ tone: "error", message: "不能降级自身管理员角色" }); return; } } if (form.password && (form.password.length < 8 || form.password.length > 72)) { onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" }); return; } if (!editing && form.role_code === "admin") { onNotify({ tone: "error", message: "新建用户不能指定管理员角色" }); return; } setSaving(true); try { if (editing) { const updated = await api.updatePlatformEmployee(editing.user_id, { display_name: form.display_name.trim(), email: form.email.trim() || null, role_code: form.role_code, status: form.status, }); setEmployees((current) => current.map((item) => (item.user_id === updated.user_id ? updated : item)), ); onNotify({ tone: "success", message: "用户信息已更新" }); } else { const created = await api.createPlatformEmployee({ username: form.username.trim(), display_name: form.display_name.trim(), email: form.email.trim() || undefined, password: form.password, role_code: form.role_code, }); if (selectedWorkspaceIds.length > 0) { const results = await Promise.allSettled( selectedWorkspaceIds.map((workspaceId) => api.addWorkspaceMember(workspaceId, { user_id: created.user_id }), ), ); const failed = results.filter((item) => item.status === "rejected").length; const joined = results.length - failed; if (failed === 0) { onNotify({ tone: "success", message: `用户已添加,并加入 ${joined} 个项目`, }); } else { onNotify({ tone: "error", message: `用户已创建,但有 ${failed} 个项目加入失败`, }); } } else { onNotify({ tone: "info", message: "用户已添加", }); } await refreshFromStart(); } setDialogOpen(false); } catch (error) { onNotify({ tone: "error", message: error instanceof ApiRequestError ? error.message : "保存用户失败", }); } finally { setSaving(false); } }; const executeRemove = async (employee: Employee): Promise => { try { await api.deletePlatformEmployee(employee.user_id); onNotify({ tone: "success", message: "用户已删除" }); if (employees.length <= 1 && page > 1) { goPrev(); } else { reload(); } } catch (error) { onNotify({ tone: "error", message: error instanceof Error ? error.message : "删除用户失败", }); } }; const executeResetPassword = async (newPassword: string): Promise => { if (!resetTarget) return; setResetSaving(true); try { await api.resetPlatformEmployeePassword(resetTarget.user_id, newPassword); onNotify({ tone: "success", message: "密码已重置" }); setResetTarget(null); } catch (error) { onNotify({ tone: "error", message: error instanceof Error ? error.message : "重置密码失败", }); } finally { setResetSaving(false); } }; const emptyMessage = useMemo(() => { if (loading) return "正在加载用户…"; if (debouncedSearch.trim()) return "未找到匹配的用户"; return "暂无用户"; }, [loading, debouncedSearch]); return (
setUserSearchTerm(event.target.value)} />
{!canManage &&
当前为开发人员,只能查看用户列表。
} 用户 账号 角色 状态 操作 {loading || employees.length === 0 ? (

{emptyMessage}

) : ( employees.map((employee) => { const isProtectedAdmin = employee.role_code === "admin"; return ( {employee.display_name.slice(0, 1)}{employee.display_name}{employee.email ?? "未设置邮箱"} {employee.username} {employee.role_code ? ({employee.role_name}) : (-)} {employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"} ); }) )}
void submit(event)} onClose={() => setDialogOpen(false)} onResetPassword={ editing ? () => { setResetTarget(editing); setDialogOpen(false); } : undefined } /> { if (!nextOpen) setDeleteTarget(null); }} title="确定删除用户?" description={ deleteTarget ? `确定从平台删除用户"${deleteTarget.display_name}"吗?` : "" } confirmLabel="删除" destructive onConfirm={async () => { if (!deleteTarget) return; await executeRemove(deleteTarget); setDeleteTarget(null); }} /> setResetTarget(null)} />
); }