"use client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; import { ChangeEvent, FormEvent, useCallback, useMemo, useState } from "react"; import { useAuth } from "@/components/auth-provider"; import { Button, TextField } from "@radix-ui/themes"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import type { RoleItem, RoleListResponse, UserListResponse, UserPublic } from "@/types/auth"; type UserRolePayload = { role_codes: string[]; }; export default function AdminUsersPage() { const { user, initializing, fetchWithAuth, hasPermission } = useAuth(); const queryClient = useQueryClient(); const [savingUserId, setSavingUserId] = useState(null); const [deletingUserId, setDeletingUserId] = useState(null); const [resettingUserId, setResettingUserId] = useState(null); const [newUserId, setNewUserId] = useState(""); const [newEmail, setNewEmail] = useState(""); const [newUsername, setNewUsername] = useState(""); const [newPassword, setNewPassword] = useState(""); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const canManage = hasPermission("user.manage"); const canReadRoles = hasPermission("role.read") || hasPermission("role.manage"); const usersPath = "/api/v1/users?limit=200&offset=0"; const rolesPath = "/api/v1/admin/roles"; const loadUsers = useCallback(async () => { const response = await fetchWithAuth(usersPath); if (!response.ok) throw new Error(await readApiError(response)); return (await response.json()) as UserListResponse; }, [fetchWithAuth]); const loadRoles = useCallback(async () => { const response = await fetchWithAuth(rolesPath); if (!response.ok) throw new Error(await readApiError(response)); return (await response.json()) as RoleListResponse; }, [fetchWithAuth]); const usersQuery = useQuery({ queryKey: [usersPath], queryFn: loadUsers, enabled: !!user && canManage, }); const rolesQuery = useQuery({ queryKey: [rolesPath], queryFn: loadRoles, enabled: !!user && canManage && canReadRoles, }); useTopicSubscription( "admin.users", useCallback(() => { if (!user || !canManage) return; void queryClient.invalidateQueries({ queryKey: [usersPath] }); if (canReadRoles) { void queryClient.invalidateQueries({ queryKey: [rolesPath] }); } }, [canManage, canReadRoles, queryClient, user]), ); const users = useMemo(() => usersQuery.data?.items ?? [], [usersQuery.data?.items]); const roles = useMemo(() => { if (canReadRoles) return rolesQuery.data?.items ?? []; return Array.from(new Set(users.flatMap((item) => item.role_codes))).map((code, index) => ({ id: -(index + 1), code, name: code, permission_codes: [], menu_ids: [], } satisfies RoleItem)); }, [canReadRoles, rolesQuery.data?.items, users]); const roleOptions = useMemo(() => roles.map((item) => item.code), [roles]); const refreshData = async () => { await queryClient.invalidateQueries({ queryKey: [usersPath] }); if (canReadRoles) { await queryClient.invalidateQueries({ queryKey: [rolesPath] }); } }; const createUserMutation = useMutation({ mutationFn: async () => { const response = await fetchWithAuth("/api/v1/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: newUserId.trim(), email: newEmail.trim(), username: newUsername.trim(), password: newPassword, }), }); if (!response.ok) throw new Error(await readApiError(response)); return response.json() as Promise; }, onSuccess: async () => { setSuccess("用户已创建"); setError(""); setNewUserId(""); setNewEmail(""); setNewUsername(""); setNewPassword(""); await refreshData(); }, onError: (candidate) => { setSuccess(""); setError(candidate instanceof Error ? candidate.message : "创建用户失败"); }, }); const updateRolesMutation = useMutation({ mutationFn: async ({ userId, roleCodes }: { userId: string; roleCodes: string[] }) => { const response = await fetchWithAuth(`/api/v1/users/${userId}/roles`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role_codes: roleCodes } satisfies UserRolePayload), }); if (!response.ok) throw new Error(await readApiError(response)); return response.json() as Promise; }, onMutate: ({ userId }) => { setSavingUserId(userId); setError(""); setSuccess(""); }, onSuccess: async () => { setSuccess("用户角色已更新"); await refreshData(); }, onError: (mutationError) => { setError(mutationError instanceof Error ? mutationError.message : "更新失败"); }, onSettled: () => setSavingUserId(null), }); const resetPasswordMutation = useMutation({ mutationFn: async ({ userId, password }: { userId: string; password: string }) => { const response = await fetchWithAuth(`/api/v1/users/${userId}/password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ new_password: password }), }); if (!response.ok) throw new Error(await readApiError(response)); return response.json() as Promise; }, onMutate: ({ userId }) => { setResettingUserId(userId); setError(""); setSuccess(""); }, onSuccess: () => setSuccess("密码已重置"), onError: (candidate) => { setSuccess(""); setError(candidate instanceof Error ? candidate.message : "重置密码失败"); }, onSettled: () => setResettingUserId(null), }); const deleteUserMutation = useMutation({ mutationFn: async (userId: string) => { const response = await fetchWithAuth(`/api/v1/users/${userId}`, { method: "DELETE" }); if (!response.ok) throw new Error(await readApiError(response)); return response.json() as Promise<{ message: string }>; }, onMutate: (userId) => { setDeletingUserId(userId); setError(""); setSuccess(""); }, onSuccess: async () => { setSuccess("用户已删除"); await refreshData(); }, onError: (candidate) => { setSuccess(""); setError(candidate instanceof Error ? candidate.message : "删除用户失败"); }, onSettled: () => setDeletingUserId(null), }); const handleCreateUser = (event: FormEvent) => { event.preventDefault(); setError(""); setSuccess(""); createUserMutation.mutate(); }; const anyError = error || (usersQuery.error instanceof Error ? usersQuery.error.message : "") || (rolesQuery.error instanceof Error ? rolesQuery.error.message : ""); if (initializing || usersQuery.isLoading || rolesQuery.isLoading) { return

Loading users...

; } if (!user) { return (

请先登录后再访问用户管理页面。

返回首页
); } if (!canManage) { return (

你没有访问该页面的权限(需要 `user.manage`)。

返回首页
); } return (
{anyError &&
{anyError}
} {success &&
{success}
}

新增用户

用户 ID 由管理员手动填写,系统会校验重复。

) => setNewUserId(event.currentTarget.value)} minLength={3} maxLength={64} required /> ) => setNewEmail(event.currentTarget.value)} required /> ) => setNewUsername(event.currentTarget.value)} minLength={3} maxLength={64} required /> ) => setNewPassword(event.currentTarget.value)} minLength={8} maxLength={128} required />

用户列表

表头已中文化,支持改角色、重置密码、删除。

{users.map((item) => ( ))}
用户ID 邮箱 用户名 状态 角色 权限 操作
{item.id} {item.email} {item.username} {item.status}
{roleOptions.map((roleCode) => { const checked = item.role_codes.includes(roleCode); return ( ); })}
{item.permission_codes.join(", ") || "-"}
); }