import { useMemo, useState } from 'react'; import { Plus, Loader2, KeyRound, Trash2, ShieldCheck, Copy, CloudDownload, AlertCircle, UserCog, } from 'lucide-react'; import { toast } from 'sonner'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '@/lib/api'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; function fmtTime(v?: string): string { if (!v || v.startsWith('0001-')) return '—'; return v.replace('T', ' ').slice(0, 16); } const roleBadge = { admin: { label: '管理员', bg: '#dafbe1', color: '#1a7f37' }, user: { label: '普通用户', bg: '#f6f8fa', color: '#6e7781' }, } as const; const sourceBadge = { ldap: { label: 'LDAP 导入', bg: '#fbefff', color: '#8250df' }, local: { label: '本地账号', bg: '#ddf4ff', color: '#0969da' }, } as const; interface UserForm { username: string; nickname: string; email: string; role: string; password: string; } const emptyForm: UserForm = { username: '', nickname: '', email: '', role: 'user', password: '' }; const inputStyle = { background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)', }; interface LdapEntry { username: string; displayName?: string; email?: string; } interface ImportResult { created: number; converted: number; skipped: number; failed: number; failedUsers: string[]; } export default function UserManage() { const queryClient = useQueryClient(); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(emptyForm); const [createdResult, setCreatedResult] = useState<{ username: string; password: string } | null>(null); const [ldapOpen, setLdapOpen] = useState(false); const [selected, setSelected] = useState>(new Set()); const [importResult, setImportResult] = useState(null); const usersQuery = useQuery({ queryKey: ['users'], queryFn: apiClient.listUsers }); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings }); // LDAP 就绪(已启用且关键配置齐全)时才显示「从 LDAP 导入」入口 const ldapReady = !!(settings?.ldapEnabled && settings?.ldapHost && settings?.ldapBindDN && settings?.ldapBaseDN); const users: any[] = usersQuery.data ?? []; const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: ['users'] }); // LDAP 预览列表(仅弹窗打开且未进入结果态时请求) const ldapQuery = useQuery({ queryKey: ['ldap-users'], queryFn: apiClient.ldapListUsers, enabled: ldapOpen && !importResult, }); const ldapEntries: LdapEntry[] = (ldapQuery.data as LdapEntry[]) ?? []; // 本地用户索引(按用户名小写匹配,目录服务通常大小写不敏感) const localMap = useMemo(() => { const m = new Map(); users.forEach((u) => m.set(String(u.username || '').toLowerCase(), u)); return m; }, [users]); // ---- 新建用户 ---- const createMutation = useMutation({ mutationFn: (body: any) => apiClient.createUser(body), onSuccess: (res: any) => { invalidateUsers(); setDialogOpen(false); if (res?.initialPassword) { setCreatedResult({ username: form.username.trim(), password: String(res.initialPassword) }); } else { toast.success('用户已创建'); } }, onError: (err: Error) => toast.error(err.message), }); const submitCreate = () => { if (!form.username.trim()) { toast.error('请填写用户名'); return; } const body: any = { username: form.username.trim(), nickname: form.nickname.trim(), email: form.email.trim(), role: form.role, }; if (form.password) body.password = form.password; createMutation.mutate(body); }; // ---- 重置密码 ---- const resetMutation = useMutation({ mutationFn: (id: any) => apiClient.resetUserPassword(id), onSuccess: (res: any) => { if (res?.initialPassword) { toast.success('密码已重置', { description: `一次性密码:${res.initialPassword}(仅显示一次,请立即保存)` }); } else { toast.success('密码已重置'); } }, onError: (err: Error) => toast.error(err.message), }); // ---- 角色切换 ---- const roleMutation = useMutation({ mutationFn: (vars: { id: any; role: string }) => apiClient.updateUser(vars.id, { role: vars.role }), onSuccess: (_d, vars) => { toast.success(vars.role === 'admin' ? '已设为管理员' : '已降为普通用户'); invalidateUsers(); }, onError: (err: Error) => toast.error(err.message), }); // ---- 删除 ---- const deleteMutation = useMutation({ mutationFn: (id: any) => apiClient.deleteUser(id), onSuccess: () => { toast.success('用户已删除'); invalidateUsers(); }, onError: (err: Error) => toast.error(err.message), }); // ---- LDAP 导入 ---- const importMutation = useMutation({ mutationFn: (list: LdapEntry[]) => apiClient.ldapImport(list), onSuccess: (res: any) => { setImportResult({ created: res?.created ?? 0, converted: res?.converted ?? 0, skipped: res?.skipped ?? 0, failed: res?.failed ?? 0, failedUsers: res?.failedUsers ?? [], }); invalidateUsers(); }, onError: (err: Error) => toast.error('导入失败', { description: err.message }), }); const openLdapDialog = () => { setSelected(new Set()); setImportResult(null); setLdapOpen(true); }; const closeLdapDialog = (open: boolean) => { setLdapOpen(open); if (!open) { setSelected(new Set()); setImportResult(null); invalidateUsers(); } }; const toggleSelected = (username: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(username)) next.delete(username); else next.add(username); return next; }); }; const selectedEntries = ldapEntries.filter((e) => selected.has(e.username)); const copyText = (text: string) => { navigator.clipboard .writeText(text) .then(() => toast.success('已复制', { description: text })) .catch(() => toast.error('复制失败')); }; const importStats = importResult ? [ { key: 'created', label: '新增', value: importResult.created, bg: '#dafbe1', color: '#1a7f37' }, { key: 'converted', label: '转换', value: importResult.converted, bg: '#ddf4ff', color: '#0969da' }, { key: 'skipped', label: '跳过', value: importResult.skipped, bg: '#f6f8fa', color: '#6e7781' }, { key: 'failed', label: '失败', value: importResult.failed, bg: '#ffebe9', color: '#cf222e' }, ] : []; return (

用户管理

{users.length}
{ldapReady && ( )}
{usersQuery.isLoading ? (
加载中...
) : usersQuery.isError ? (
{(usersQuery.error as Error).message}
) : (
{['用户名', '昵称', '邮箱', '角色', '来源', '创建时间', '操作'].map((h) => ( ))} {users.map((u) => { const role = (roleBadge as any)[u.role] ?? roleBadge.user; const source = (sourceBadge as any)[u.authSource] ?? sourceBadge.local; const isAdmin = u.role === 'admin'; return ( ); })} {users.length === 0 && ( )}
{h}
{u.username} {u.nickname || '—'} {u.email || '—'} {role.label} {source.label} {fmtTime(u.createdAt)}
{u.authSource !== 'ldap' && ( )}
暂无用户,点击右上角「新建用户」创建
)}
{/* 新建用户弹窗 */} 新建用户
setForm({ ...form, username: e.target.value })} placeholder="zhangsan" />
setForm({ ...form, nickname: e.target.value })} placeholder="张三" />
setForm({ ...form, email: e.target.value })} placeholder="zhangsan@example.com" />
setForm({ ...form, password: e.target.value })} placeholder="留空由系统随机生成" />
{/* 创建成功:一次性密码 */} !open && setCreatedResult(null)}> 用户已创建

用户 {createdResult?.username} 创建成功。 系统已生成一次性初始密码,仅显示此次,请立即复制并妥善交付给用户:

{createdResult?.password}
{/* LDAP 导入弹窗 */} 从 LDAP 导入用户 {importResult ? ( /* 结果态:四色统计 + 失败名单 */
{importStats.map((s) => (
{s.value}
{s.label}
))}
{importResult.failedUsers.length > 0 && (
以下用户导入失败:
{importResult.failedUsers.map((name) => ( {name} ))}
)}

「转换」表示本地同名账号已切换为 LDAP 来源;「跳过」表示该用户已是 LDAP 账号。

) : ldapQuery.isLoading ? (
正在从 LDAP 目录读取用户...
) : ldapQuery.isError ? (
{(ldapQuery.error as Error).message}
请到系统设置检查 LDAP 配置
) : ( /* 选择态 */
{ldapEntries.map((entry) => { const local = localMap.get(String(entry.username || '').toLowerCase()); const isImported = local?.authSource === 'ldap'; const willConvert = !!local && local.authSource !== 'ldap'; const checked = selected.has(entry.username); return ( ); })} {ldapEntries.length === 0 && (
目录中未读取到用户
)}
共 {ldapEntries.length} 个用户,已选 {selected.size} 个
)} {importResult && ( )}
); }