'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { useAuth } from '@/components/providers/auth-provider'; import { PageHeader } from '@/components/layout/page-header'; import { AppModal } from '@/components/ui/app-modal'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { createUser, getUser, getUsers, manageUser, searchUsers, updateUser, } from '@/features/users/api/users'; import type { ManageUserAction, UserItem, UserMutationPayload, } from '@/features/users/types'; import { DangerButton, PrimaryButton, ResourceField, ResourceInput, SecondaryButton, } from '@/features/shared/components/resource-primitives'; const ITEMS_PER_PAGE = 10; const usersListQueryKey = ['users', 'list'] as const; const userSchema = z.object({ username: z.string().trim().min(1, '请输入用户名').max(12, '用户名不能超过 12 个字符'), display_name: z.string().trim().max(20, '显示名称不能超过 20 个字符'), password: z .string() .max(20, '密码不能超过 20 个字符') .refine((value) => value.length === 0 || value.length >= 8, '密码至少需要 8 个字符'), }); type UserFormValues = z.infer; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; const defaultValues: UserFormValues = { username: '', display_name: '', password: '', }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function getRoleMeta(role: number) { if (role >= 100) { return { label: '超级管理员', variant: 'warning' as const }; } if (role >= 10) { return { label: '管理员', variant: 'info' as const }; } return { label: '普通用户', variant: 'success' as const }; } function getStatusMeta(status: number) { if (status === 1) { return { label: '已激活', variant: 'success' as const }; } return { label: '已封禁', variant: 'danger' as const }; } function toPayload(values: UserFormValues): UserMutationPayload { return { username: values.username.trim(), display_name: values.display_name.trim(), password: values.password, }; } export function UsersPage() { const queryClient = useQueryClient(); const { user: currentUser } = useAuth(); const [page, setPage] = useState(0); const [searchInput, setSearchInput] = useState(''); const [searchKeyword, setSearchKeyword] = useState(''); const [editingUserId, setEditingUserId] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [feedback, setFeedback] = useState(null); const form = useForm({ resolver: zodResolver(userSchema), defaultValues, }); const isAdmin = (currentUser?.role ?? 0) >= 10; const isRoot = (currentUser?.role ?? 0) >= 100; const usersQuery = useQuery({ queryKey: [...usersListQueryKey, page], queryFn: () => getUsers(page), enabled: isAdmin && searchKeyword.length === 0, }); const searchQuery = useQuery({ queryKey: [...usersListQueryKey, 'search', searchKeyword], queryFn: () => searchUsers(searchKeyword), enabled: isAdmin && searchKeyword.length > 0, }); const editingUserQuery = useQuery({ queryKey: ['users', 'detail', editingUserId], queryFn: () => getUser(editingUserId as number), enabled: isAdmin && editingUserId !== null, }); useEffect(() => { if (editingUserQuery.data) { form.reset({ username: editingUserQuery.data.username, display_name: editingUserQuery.data.display_name || '', password: '', }); } }, [editingUserQuery.data, form]); const saveMutation = useMutation({ mutationFn: async (values: UserFormValues) => { if (!editingUserId && values.password.length === 0) { throw new Error('创建用户时必须填写密码。'); } const payload = toPayload(values); if (editingUserId) { await updateUser({ id: editingUserId, ...payload }); return '用户信息已更新。'; } await createUser(payload); return '用户已创建。'; }, onSuccess: async (message) => { setFeedback({ tone: 'success', message }); setEditingUserId(null); setIsEditorOpen(false); form.reset(defaultValues); await Promise.all([ queryClient.invalidateQueries({ queryKey: usersListQueryKey }), queryClient.invalidateQueries({ queryKey: ['users', 'detail'] }), ]); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const manageMutation = useMutation({ mutationFn: async ({ username, action }: { username: string; action: ManageUserAction }) => { await manageUser(username, action); return action; }, onSuccess: async (action) => { const actionLabel = { promote: '提升权限', demote: '降级权限', delete: '删除用户', disable: '禁用用户', enable: '启用用户', }[action]; setFeedback({ tone: 'success', message: `${actionLabel}操作已完成。` }); await Promise.all([ queryClient.invalidateQueries({ queryKey: usersListQueryKey }), queryClient.invalidateQueries({ queryKey: ['users', 'detail'] }), ]); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const activeQuery = searchKeyword.length > 0 ? searchQuery : usersQuery; const users = useMemo(() => activeQuery.data ?? [], [activeQuery.data]); const handleSearchSubmit = () => { setFeedback(null); setPage(0); setSearchKeyword(searchInput.trim()); }; const handleResetSearch = () => { setSearchInput(''); setSearchKeyword(''); setPage(0); setFeedback(null); }; const handleResetForm = () => { setEditingUserId(null); setIsEditorOpen(false); setFeedback(null); form.reset(defaultValues); }; const handleCreate = () => { setEditingUserId(null); setFeedback(null); form.reset(defaultValues); setIsEditorOpen(true); }; const handleEdit = (targetUser: UserItem) => { setFeedback(null); setEditingUserId(targetUser.id); setIsEditorOpen(true); }; const handleManage = (targetUser: UserItem, action: ManageUserAction) => { const actionText = { promote: '提升', demote: '降级', delete: '删除', disable: '禁用', enable: '启用', }[action]; if (action === 'delete' && !window.confirm(`确认删除用户“${targetUser.username}”吗?`)) { return; } if ((action === 'disable' || action === 'enable') && !window.confirm(`确认${actionText}用户“${targetUser.username}”吗?`)) { return; } setFeedback(null); manageMutation.mutate({ username: targetUser.username, action }); }; const handleSubmit = form.handleSubmit((values) => { setFeedback(null); saveMutation.mutate(values); }); if (!isAdmin) { return (
); } return ( <>
新建用户 } /> {feedback ? : null} void queryClient.invalidateQueries({ queryKey: usersListQueryKey })} > 刷新
} >
setSearchInput(event.target.value)} placeholder='搜索用户 ID、用户名、显示名称或邮箱地址' className='md:max-w-xl' />
搜索 清空
{searchKeyword.length === 0 ? (
setPage((value) => Math.max(value - 1, 0))} disabled={page === 0 || activeQuery.isLoading}> 上一页 setPage((value) => value + 1)} disabled={activeQuery.isLoading || users.length < ITEMS_PER_PAGE} > 下一页
) : null}
{activeQuery.isLoading ? ( ) : activeQuery.isError ? ( ) : users.length === 0 ? ( ) : (
{users.map((targetUser) => { const roleMeta = getRoleMeta(targetUser.role); const statusMeta = getStatusMeta(targetUser.status); const canManage = (currentUser?.role ?? 0) > targetUser.role; return ( ); })}
用户 邮箱 角色 状态 操作

{targetUser.username}

显示名称:{targetUser.display_name || '未设置'}

ID:{targetUser.id}

{targetUser.email || '未绑定'}
handleEdit(targetUser)} className='px-3 py-2 text-xs' disabled={!canManage}> 编辑 handleManage(targetUser, targetUser.status === 1 ? 'disable' : 'enable')} className='px-3 py-2 text-xs' disabled={!canManage || manageMutation.isPending} > {targetUser.status === 1 ? '禁用' : '启用'} handleManage(targetUser, targetUser.role >= 10 ? 'demote' : 'promote')} className='px-3 py-2 text-xs' disabled={!canManage || manageMutation.isPending || (!isRoot && targetUser.role < 10)} > {targetUser.role >= 10 ? '降级' : '提升'} handleManage(targetUser, 'delete')} className='px-3 py-2 text-xs' disabled={!canManage || manageMutation.isPending} > 删除
)}
取消 {saveMutation.isPending ? '保存中...' : editingUserId ? '保存修改' : '创建用户'} } > {editingUserId && editingUserQuery.isLoading ? ( ) : editingUserId && editingUserQuery.isError ? ( ) : (
{editingUserId && editingUserQuery.data ? (

绑定邮箱

{editingUserQuery.data.email || '未绑定'}

第三方账号

GitHub:{editingUserQuery.data.github_id || '未绑定'}

微信:{editingUserQuery.data.wechat_id || '未绑定'}

) : null}
)}
); }