"use client" import { type KeyboardEvent, useCallback, useEffect, useState } from "react" import { KeyRoundIcon, MoreHorizontalIcon, PlusIcon, RefreshCwIcon, SearchIcon, ShieldIcon, UserRoundIcon, } from "lucide-react" import { toast } from "sonner" import { assignUserRoles, createUser, fetchRoleListAll, fetchUserDetail, fetchUsers, resetUserPassword, updateUser, updateUserStatus, type AdminRole, type AdminUser, type CreateAdminUserPayload, type PageResult, type ResetPasswordResult, type UpdateAdminUserPayload, } from "@/lib/api/admin" import { Status } from "@/lib/generated/enums" import { formatDateTime } from "@/lib/utils" import { AssignRolesDrawer } from "./_components/assign-roles" import { CreateUserDrawer } from "./_components/create" import { EditDrawer } from "./_components/edit" import { InitialPasswordDialog } from "./_components/initial-password-dialog" import { ResetPasswordDialogs } from "./_components/reset-password" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { ButtonGroup } from "@/components/ui/button-group" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { ListPagination } from "@/components/list-pagination" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" export default function DashboardUsersPage() { const [keywordInput, setKeywordInput] = useState("") const [keyword, setKeyword] = useState("") const [page, setPage] = useState(1) const [limit, setLimit] = useState(20) const [loading, setLoading] = useState(true) const [creatingOpen, setCreatingOpen] = useState(false) const [savingCreate, setSavingCreate] = useState(false) const [initialPassword, setInitialPassword] = useState<{ username: string password: string } | null>(null) const [savingEdit, setSavingEdit] = useState(false) const [savingPassword, setSavingPassword] = useState(false) const [savingRoles, setSavingRoles] = useState(false) const [editingUser, setEditingUser] = useState(null) const [resettingUser, setResettingUser] = useState(null) const [assigningRolesUser, setAssigningRolesUser] = useState(null) const [assignRoleOptions, setAssignRoleOptions] = useState([]) const [assignRoleIds, setAssignRoleIds] = useState([]) const [assignRolesLoading, setAssignRolesLoading] = useState(false) const [resetPasswordResult, setResetPasswordResult] = useState(null) const [actionLoadingId, setActionLoadingId] = useState(null) const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }) const loadUsers = useCallback(async () => { setLoading(true) try { const data = await fetchUsers({ username: keyword.trim() || undefined, page, limit, }) setResult(data) } catch (error) { toast.error(error instanceof Error ? error.message : "加载用户失败") } finally { setLoading(false) } }, [keyword, limit, page]) useEffect(() => { void loadUsers() }, [loadUsers]) function applyFilters() { setKeyword(keywordInput) setPage(1) } function handleFilterKeyDown(event: KeyboardEvent) { if (event.key !== "Enter") { return } event.preventDefault() applyFilters() } function openEditDrawer(user: AdminUser) { setEditingUser(user) } async function openAssignRolesDrawer(user: AdminUser) { setActionLoadingId(user.id) setAssigningRolesUser(user) setAssignRolesLoading(true) try { const [roles, userDetail] = await Promise.all([ fetchRoleListAll(), fetchUserDetail(user.id), ]) setAssignRoleOptions(roles) setAssignRoleIds((userDetail.roles || []).map((role) => role.id)) } catch (error) { setAssigningRolesUser(null) toast.error(error instanceof Error ? error.message : "加载角色分配数据失败") } finally { setAssignRolesLoading(false) setActionLoadingId(null) } } function handlePageChange(nextPage: number) { if (nextPage < 1 || nextPage === page) { return } setPage(nextPage) } function handleLimitChange(nextLimit: number) { if (nextLimit <= 0 || nextLimit === limit) { return } setLimit(nextLimit) setPage(1) } function handleEditDrawerOpenChange(open: boolean) { if (savingEdit) { return } if (!open) { setEditingUser(null) } } function handleCreateDrawerOpenChange(open: boolean) { if (savingCreate) { return } if (!open) { setCreatingOpen(false) } } async function handleCreateUser(payload: CreateAdminUserPayload) { if (savingCreate) { return } setSavingCreate(true) try { const result = await createUser(payload) toast.success(`已创建用户 ${result.user.username}`) setCreatingOpen(false) setInitialPassword({ username: result.user.username, password: result.password, }) await loadUsers() } catch (error) { toast.error(error instanceof Error ? error.message : "创建用户失败") } finally { setSavingCreate(false) } } function handleAssignRolesOpenChange(open: boolean) { if (savingRoles) { return } if (!open) { setAssigningRolesUser(null) setAssignRoleOptions([]) setAssignRoleIds([]) } } async function handleSaveUser(payload: UpdateAdminUserPayload) { if (savingEdit) { return } setSavingEdit(true) try { await updateUser(payload) toast.success(`已更新 ${editingUser?.username || "用户"}`) setEditingUser(null) await loadUsers() } catch (error) { toast.error(error instanceof Error ? error.message : "更新用户失败") } finally { setSavingEdit(false) } } async function handleAssignRoles(roleIds: number[]) { if (!assigningRolesUser || savingRoles) { return } setSavingRoles(true) try { await assignUserRoles(assigningRolesUser.id, roleIds) toast.success(`已更新 ${assigningRolesUser.username} 的角色`) setAssigningRolesUser(null) setAssignRoleOptions([]) setAssignRoleIds([]) await loadUsers() } catch (error) { toast.error(error instanceof Error ? error.message : "保存角色分配失败") } finally { setSavingRoles(false) } } function openResetDrawer(user: AdminUser) { setResetPasswordResult(null) setResettingUser(user) } function handleResetDrawerOpenChange(open: boolean) { if (savingPassword) { return } if (!open) { setResetPasswordResult(null) setResettingUser(null) } } async function handleResetPassword() { if (!resettingUser || savingPassword) { return } setSavingPassword(true) try { const result = await resetUserPassword(resettingUser.id) setResetPasswordResult(result) toast.success(`已重置 ${resettingUser.username} 的密码`) } catch (error) { toast.error(error instanceof Error ? error.message : "重置密码失败") } finally { setSavingPassword(false) } } async function handleToggleStatus(user: AdminUser) { setActionLoadingId(user.id) try { const nextStatus = user.status === Status.Ok ? Status.Disabled : Status.Ok await updateUserStatus(user.id, nextStatus) toast.success(`${user.username} 已${nextStatus === Status.Ok ? "启用" : "禁用"}`) await loadUsers() } catch (error) { toast.error(error instanceof Error ? error.message : "更新状态失败") } finally { setActionLoadingId(null) } } return ( <>
setKeywordInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder="按用户名筛选" className="pl-9" />
用户 角色 状态 最后登录 联系方式 操作 {result.results.map((item) => (
{item.nickname || item.username}
{item.username}
{(item.roles || []).length > 0 ? ( item.roles?.map((role) => ( {role.name} )) ) : ( 未分配 )}
{item.status === Status.Ok ? "启用" : "禁用"} {item.isSystem ? ( 系统 ) : null}
{formatDateTime(item.lastLoginAt)}
{item.lastLoginIp || "-"}
{item.mobile || "-"}
{item.email || "-"}
} aria-label={`更多操作 ${item.username}`} > void openAssignRolesDrawer(item)} disabled={actionLoadingId === item.id} > {actionLoadingId === item.id ? "处理中..." : "分配角色"} openResetDrawer(item)} disabled={actionLoadingId === item.id} > 重置密码 handleToggleStatus(item)} disabled={actionLoadingId === item.id} > {actionLoadingId === item.id ? "处理中..." : item.status === Status.Ok ? "禁用" : "启用"}
))} {!loading && result.results.length === 0 ? ( 没有匹配的用户数据 ) : null}
{ if (!open) { setInitialPassword(null) } }} /> ) }