import { useState, useEffect, useCallback, useMemo } from 'react'; import { Plus, Search, Edit2, Trash2, XCircle, Eye, Download, Upload, CheckSquare, Square, X, RotateCcw, KeyRound, ChevronDown, UserCheck, UserX, Filter, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { userApi } from '@/api/user'; /* ───────── Types ───────── */ interface UserItem { id: number; username: string; display_name: string; email: string | null; role: string; is_active: boolean; created_at: string; last_login?: string | null; } interface UserActivity { id: string; action: string; target: string; created_at: string; status: 'success' | 'failed'; } interface FormErrors { username?: string; password?: string; email?: string; } /* ───────── Constants ───────── */ const roleNameMap: Record = { admin: '超级管理员', dba: '数据库管理员', developer: '开发人员', guest: '访客', }; const roleTagStyle: Record = { admin: 'bg-purple-500/15 text-purple-400', dba: 'bg-blue-500/15 text-blue-400', developer: 'bg-green-500/15 text-green-400', guest: 'bg-[var(--dg-text-muted)]/15 text-[var(--dg-text-muted)]', }; const roleOptions = [ { value: '', label: '全部角色' }, { value: 'admin', label: '超级管理员' }, { value: 'dba', label: '数据库管理员' }, { value: 'developer', label: '开发人员' }, { value: 'auditor', label: '审计员' }, ]; const statusOptions = [ { value: '', label: '全部状态' }, { value: 'active', label: '启用' }, { value: 'disabled', label: '禁用' }, ]; const BATCH_OPTIONS = [ { key: 'enable', label: '批量启用', icon: UserCheck }, { key: 'disable', label: '批量禁用', icon: UserX }, { key: 'delete', label: '批量删除', icon: Trash2 }, ] as const; /* ───────── Validation ───────── */ function validateUserForm(data: { username: string; password: string; email: string }): FormErrors { const errors: FormErrors = {}; if (!data.username.trim()) { errors.username = '用户名不能为空'; } else if (!/^[a-zA-Z0-9_]{3,30}$/.test(data.username)) { errors.username = '用户名只能包含字母、数字和下划线,长度3-30'; } if (!data.password) { errors.password = '密码不能为空'; } else if (data.password.length < 6) { errors.password = '密码长度至少6位'; } if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { errors.email = '邮箱格式不正确'; } return errors; } /* ───────── CSV Export Helper ───────── */ function exportUsersToCSV(users: UserItem[]) { const headers = ['用户名', '显示名称', '邮箱', '角色', '状态', '创建时间']; const rows = users.map(u => [ u.username, u.display_name || '', u.email || '', roleNameMap[u.role] || u.role, u.is_active ? '启用' : '禁用', u.created_at, ]); const csv = [headers, ...rows].map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n'); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `users_export_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } /* ───────── Main Component ───────── */ export default function UsersPage() { const [search, setSearch] = useState(''); const [roleFilter, setRoleFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [loginTimeFilter, setLoginTimeFilter] = useState(''); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [showCreate, setShowCreate] = useState(false); const [showDetail, setShowDetail] = useState(null); const [showEdit, setShowEdit] = useState(null); const [showResetPwd, setShowResetPwd] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showBatchMenu, setShowBatchMenu] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const fetchUsers = useCallback(async () => { setLoading(true); try { const res = await userApi.list(); const data = (res as any)?.items || res; setUsers(Array.isArray(data) ? data : []); } catch { setToast({ message: '获取用户列表失败', type: 'error' }); } finally { setLoading(false); } }, []); useEffect(() => { fetchUsers(); }, [fetchUsers]); // Auto-dismiss toast useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 3000); return () => clearTimeout(t); }, [toast]); const filtered = useMemo(() => { return users.filter(u => { if (roleFilter && u.role !== roleFilter) return false; if (statusFilter === 'active' && !u.is_active) return false; if (statusFilter === 'disabled' && u.is_active) return false; if (search && !u.username.toLowerCase().includes(search.toLowerCase()) && !(u.email || '').toLowerCase().includes(search.toLowerCase()) && !(u.display_name || '').toLowerCase().includes(search.toLowerCase())) return false; if (loginTimeFilter && u.last_login) { const days = Math.floor((Date.now() - new Date(u.last_login).getTime()) / 86400000); if (loginTimeFilter === '7d' && days > 7) return false; if (loginTimeFilter === '30d' && days > 30) return false; if (loginTimeFilter === '90d' && days > 90) return false; } return true; }); }, [users, roleFilter, statusFilter, search, loginTimeFilter]); const allSelected = filtered.length > 0 && filtered.every(u => selectedIds.has(u.id)); const toggleSelectAll = () => { if (allSelected) { const next = new Set(selectedIds); filtered.forEach(u => next.delete(u.id)); setSelectedIds(next); } else { const next = new Set(selectedIds); filtered.forEach(u => next.add(u.id)); setSelectedIds(next); } }; const toggleSelect = (id: number) => { const next = new Set(selectedIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedIds(next); }; const handleToggleActive = async (user: UserItem) => { try { await userApi.update(user.id, { is_active: !user.is_active }); setToast({ message: `用户已${!user.is_active ? '启用' : '禁用'}`, type: 'success' }); fetchUsers(); } catch { setToast({ message: '操作失败', type: 'error' }); } }; const handleDelete = async (id: number) => { if (!confirm('确定删除该用户?此操作不可恢复。')) return; try { await userApi.delete(id); setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n; }); setToast({ message: '用户已删除', type: 'success' }); fetchUsers(); } catch { setToast({ message: '删除失败', type: 'error' }); } }; const handleBatchAction = async (action: string) => { const ids = Array.from(selectedIds); if (ids.length === 0) return; if (action === 'delete' && !confirm(`确定删除选中的 ${ids.length} 个用户?此操作不可恢复。`)) return; try { if (action === 'enable') { await Promise.all(ids.map(id => userApi.update(id, { is_active: true }))); setToast({ message: `已启用 ${ids.length} 个用户`, type: 'success' }); } else if (action === 'disable') { await Promise.all(ids.map(id => userApi.update(id, { is_active: false }))); setToast({ message: `已禁用 ${ids.length} 个用户`, type: 'success' }); } else if (action === 'delete') { await Promise.all(ids.map(id => userApi.delete(id))); setToast({ message: `已删除 ${ids.length} 个用户`, type: 'success' }); } setSelectedIds(new Set()); fetchUsers(); } catch { setToast({ message: '批量操作失败', type: 'error' }); } }; const handleImport = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { // Simulate import setToast({ message: `文件 "${file.name}" 导入成功`, type: 'success' }); fetchUsers(); } catch { setToast({ message: '导入失败,请检查文件格式', type: 'error' }); } }; reader.readAsText(file); e.target.value = ''; }; return (
{/* Toast */} {toast && (
{toast.type === 'success' ? : } {toast.message}
)} {/* Header */}

用户管理

管理系统用户,分配角色和数据库访问权限

{/* Filters */}
setSearch(e.target.value)} />
{(search || roleFilter || statusFilter || loginTimeFilter) && ( )}
{/* Batch Actions Bar */} {selectedIds.size > 0 && (
已选择 {selectedIds.size} 项
{showBatchMenu && ( <>
setShowBatchMenu(false)} />
{BATCH_OPTIONS.map(opt => { const Icon = opt.icon; return ( ); })}
)}
)} {/* User Table */} {loading && users.length === 0 ? (
加载中...
) : filtered.length === 0 ? (

没有找到匹配的用户

{(search || roleFilter || statusFilter || loginTimeFilter) && ( )}
) : ( <>
{filtered.map(user => { const isSelected = selectedIds.has(user.id); return ( ); })}
用户 角色 邮箱 最后登录 状态 操作
setShowDetail(user)}>
{(user.display_name || user.username).charAt(0)}
{user.display_name || user.username}
@{user.username}
{roleNameMap[user.role] || user.role} {user.email || '-'} {user.last_login ? new Date(user.last_login).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '-'}
共 {filtered.length} 条记录 {selectedIds.size > 0 && `,已选择 ${selectedIds.size} 项`}
)} {/* Create Modal */} {showCreate && ( setShowCreate(false)} onCreated={() => { setShowCreate(false); fetchUsers(); }} setToast={setToast} /> )} {/* Detail Modal */} {showDetail && ( setShowDetail(null)} /> )} {/* Edit Modal */} {showEdit && ( setShowEdit(null)} onUpdated={() => { setShowEdit(null); fetchUsers(); }} setToast={setToast} /> )} {/* Reset Password Modal */} {showResetPwd && ( setShowResetPwd(null)} setToast={setToast} /> )}
); } /* ───────── Create User Modal ───────── */ function CreateUserModal({ onClose, onCreated, setToast }: { onClose: () => void; onCreated: () => void; setToast: (t: { message: string; type: 'success' | 'error' }) => void }) { const [username, setUsername] = useState(''); const [displayName, setDisplayName] = useState(''); const [password, setPassword] = useState(''); const [email, setEmail] = useState(''); const [role, setRole] = useState('developer'); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState({}); const handleSubmit = async () => { const validation = validateUserForm({ username, password, email }); setErrors(validation); if (Object.keys(validation).length > 0) return; setSubmitting(true); try { await userApi.create({ username, display_name: displayName, password, email: email || undefined, role }); setToast({ message: '用户创建成功', type: 'success' }); onCreated(); } catch (e: any) { setToast({ message: e?.response?.data?.detail || '创建失败', type: 'error' }); } finally { setSubmitting(false); } }; return (
e.stopPropagation()}>

新建用户

{ setUsername(e.target.value); if (errors.username) setErrors(p => ({ ...p, username: undefined })); }} className={`w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border rounded-lg text-sm focus:border-blue-400 focus:outline-none ${errors.username ? 'border-red-400' : 'border-[var(--dg-border)]'}`} placeholder="3-30位字母、数字或下划线" /> {errors.username &&

{errors.username}

}
setDisplayName(e.target.value)} className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="用户的显示名称" />
{ setPassword(e.target.value); if (errors.password) setErrors(p => ({ ...p, password: undefined })); }} className={`w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border rounded-lg text-sm focus:border-blue-400 focus:outline-none ${errors.password ? 'border-red-400' : 'border-[var(--dg-border)]'}`} placeholder="至少6位" /> {errors.password &&

{errors.password}

}
{ setEmail(e.target.value); if (errors.email) setErrors(p => ({ ...p, email: undefined })); }} className={`w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border rounded-lg text-sm focus:border-blue-400 focus:outline-none ${errors.email ? 'border-red-400' : 'border-[var(--dg-border)]'}`} placeholder="example@company.com" /> {errors.email &&

{errors.email}

}
); } /* ───────── Edit User Modal ───────── */ function EditUserModal({ user, onClose, onUpdated, setToast }: { user: UserItem; onClose: () => void; onUpdated: () => void; setToast: (t: { message: string; type: 'success' | 'error' }) => void }) { const [displayName, setDisplayName] = useState(user.display_name || ''); const [email, setEmail] = useState(user.email || ''); const [role, setRole] = useState(user.role); const [isActive, setIsActive] = useState(user.is_active); const [submitting, setSubmitting] = useState(false); const handleSubmit = async () => { setSubmitting(true); try { await userApi.update(user.id, { display_name: displayName, email: email || undefined, role, is_active: isActive }); setToast({ message: '用户更新成功', type: 'success' }); onUpdated(); } catch { setToast({ message: '更新失败', type: 'error' }); } finally { setSubmitting(false); } }; return (
e.stopPropagation()}>

编辑用户

{(user.display_name || user.username).charAt(0)}
{user.display_name || user.username}
@{user.username}
setDisplayName(e.target.value)} className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" />
setEmail(e.target.value)} className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" />
{isActive ? '已启用' : '已禁用'}
); } /* ───────── Reset Password Modal ───────── */ function ResetPasswordModal({ user, onClose, setToast }: { user: UserItem; onClose: () => void; setToast: (t: { message: string; type: 'success' | 'error' }) => void }) { const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const handleSubmit = async () => { setError(''); if (!newPassword) { setError('请输入新密码'); return; } if (newPassword.length < 6) { setError('密码长度至少6位'); return; } if (newPassword !== confirmPassword) { setError('两次输入的密码不一致'); return; } setSubmitting(true); try { await userApi.update(user.id, { password: newPassword }); setToast({ message: '密码重置成功', type: 'success' }); onClose(); } catch { setToast({ message: '密码重置失败', type: 'error' }); } finally { setSubmitting(false); } }; return (
e.stopPropagation()}>

重置密码

为用户 {user.display_name || user.username} 设置新密码

{ setNewPassword(e.target.value); setError(''); }} className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="至少6位" />
{ setConfirmPassword(e.target.value); setError(''); }} className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="再次输入" />
{error &&

{error}

}
); } /* ───────── User Detail Modal ───────── */ function UserDetailModal({ user, onClose }: { user: UserItem; onClose: () => void }) { return (
e.stopPropagation()}>

用户详情

{/* User Info Card */}
{(user.display_name || user.username).charAt(0)}
{user.display_name || user.username}
@{user.username}
{roleNameMap[user.role] || user.role} {user.is_active ? '启用' : '禁用'}
{/* Detail Fields */}
邮箱
{user.email || '-'}
创建时间
{new Date(user.created_at).toLocaleDateString('zh-CN')}
最后登录
{user.last_login ? new Date(user.last_login).toLocaleString('zh-CN') : '-'}
用户ID
#{user.id}
{/* Activity Log */}

最近活动

暂无活动记录
); } /* ───────── Need Clock icon for detail modal ───────── */ function Clock(props: React.SVGProps) { return ( ); }