import { useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { ChevronLeft, ChevronRight, Pencil, Plus, RefreshCw, Search, Trash2, UserRound, X } from 'lucide-react'; import { APP_ENDPOINTS } from '../../../config/env'; import { LucentDrawer } from '../../../components/lucent/LucentDrawer'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import type { BotState } from '../../../types/bot'; import type { SysRoleSummary, SysUserSummary } from '../../../types/sys'; import { normalizePlatformPageSize, readCachedPlatformPageSize, writeCachedPlatformPageSize } from '../../../utils/platformPageSize'; interface UserManagementPageProps { isZh: boolean; } interface UserDraft { username: string; display_name: string; password: string; role_id: number; is_active: boolean; bot_ids: string[]; } const emptyDraft: UserDraft = { username: '', display_name: '', password: '', role_id: 0, is_active: true, bot_ids: [], }; function formatLastLogin(value: string | null | undefined, isZh: boolean) { if (!value) return isZh ? '从未登录' : 'Never logged in'; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value; return parsed.toLocaleString(isZh ? 'zh-CN' : 'en-US', { hour12: false, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } export function UserManagementPage({ isZh }: UserManagementPageProps) { const { notify, confirm } = useLucentPrompt(); const [items, setItems] = useState([]); const [roles, setRoles] = useState([]); const [bots, setBots] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [togglingUserId, setTogglingUserId] = useState(null); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(() => readCachedPlatformPageSize(10)); const [editorOpen, setEditorOpen] = useState(false); const [editingUserId, setEditingUserId] = useState(null); const [draft, setDraft] = useState(emptyDraft); const roleOptions = useMemo( () => roles.filter((role) => role.is_active).sort((a, b) => a.sort_order - b.sort_order || a.id - b.id), [roles], ); const loadAll = async () => { setLoading(true); try { const [usersRes, rolesRes, botsRes, settingsRes] = await Promise.all([ axios.get<{ items: SysUserSummary[] }>(`${APP_ENDPOINTS.apiBase}/sys/users`), axios.get<{ items: SysRoleSummary[] }>(`${APP_ENDPOINTS.apiBase}/sys/roles`), axios.get(`${APP_ENDPOINTS.apiBase}/bots`), axios.get<{ page_size?: number }>(`${APP_ENDPOINTS.apiBase}/platform/settings`), ]); setItems(Array.isArray(usersRes.data?.items) ? usersRes.data.items : []); setRoles(Array.isArray(rolesRes.data?.items) ? rolesRes.data.items : []); setBots(Array.isArray(botsRes.data) ? botsRes.data : []); const normalized = normalizePlatformPageSize(settingsRes.data?.page_size, readCachedPlatformPageSize(10)); writeCachedPlatformPageSize(normalized); setPageSize(normalized); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '读取用户管理失败。' : 'Failed to load user management.'), { tone: 'error' }); } finally { setLoading(false); } }; useEffect(() => { void loadAll(); }, []); useEffect(() => { setPage(1); }, [search, pageSize, items.length]); const filteredItems = useMemo(() => { const keyword = search.trim().toLowerCase(); if (!keyword) return items; return items.filter((item) => [item.username, item.display_name, item.role?.name, item.role?.role_key].some((value) => String(value || '').toLowerCase().includes(keyword), ), ); }, [items, search]); const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize)); const currentPage = Math.min(page, pageCount); const pagedItems = useMemo( () => filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize), [currentPage, filteredItems, pageSize], ); const openCreate = () => { setEditingUserId(null); setDraft({ ...emptyDraft, role_id: roleOptions[0]?.id || 0, bot_ids: [], }); setEditorOpen(true); }; const openEdit = (item: SysUserSummary) => { setEditingUserId(item.id); setDraft({ username: item.username, display_name: item.display_name || item.username, password: '', role_id: Number(item.role?.id || roleOptions[0]?.id || 0), is_active: item.is_active !== false, bot_ids: [...(item.bot_ids || [])], }); setEditorOpen(true); }; const handleSave = async () => { const payload = { display_name: draft.display_name.trim(), password: draft.password, role_id: Number(draft.role_id || 0), is_active: draft.is_active, bot_ids: [...draft.bot_ids], }; if (!payload.display_name) { notify(isZh ? '请填写显示名称。' : 'Display name is required.', { tone: 'warning' }); return; } if (!payload.role_id) { notify(isZh ? '请选择角色。' : 'Select a role.', { tone: 'warning' }); return; } if (editingUserId == null) { if (!draft.username.trim()) { notify(isZh ? '请填写用户名。' : 'Username is required.', { tone: 'warning' }); return; } if (draft.password.trim().length < 6) { notify(isZh ? '初始化密码至少 6 位。' : 'Initial password must be at least 6 characters.', { tone: 'warning' }); return; } } setSaving(true); try { if (editingUserId == null) { await axios.post(`${APP_ENDPOINTS.apiBase}/sys/users`, { username: draft.username.trim().toLowerCase(), ...payload, }); notify(isZh ? '用户已创建。' : 'User created.', { tone: 'success' }); } else { await axios.put(`${APP_ENDPOINTS.apiBase}/sys/users/${editingUserId}`, payload); notify(isZh ? '用户已更新。' : 'User updated.', { tone: 'success' }); } setEditorOpen(false); setDraft(emptyDraft); setEditingUserId(null); await loadAll(); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '保存用户失败。' : 'Failed to save user.'), { tone: 'error' }); } finally { setSaving(false); } }; const handleToggleActive = async (item: SysUserSummary, checked: boolean) => { const roleId = Number(item.role?.id || 0); if (!roleId) { notify(isZh ? '当前用户未绑定有效角色,无法修改启停状态。' : 'This user has no valid role assigned.', { tone: 'warning' }); return; } setTogglingUserId(item.id); try { await axios.put(`${APP_ENDPOINTS.apiBase}/sys/users/${item.id}`, { display_name: String(item.display_name || item.username || '').trim() || item.username, password: '', role_id: roleId, is_active: checked, bot_ids: [...(item.bot_ids || [])], }); setItems((prev) => prev.map((entry) => (entry.id === item.id ? { ...entry, is_active: checked } : entry))); notify(checked ? (isZh ? '用户已启用。' : 'User enabled.') : (isZh ? '用户已停用。' : 'User disabled.'), { tone: 'success' }); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '更新用户状态失败。' : 'Failed to update user status.'), { tone: 'error' }); await loadAll(); } finally { setTogglingUserId(null); } }; const handleDelete = async (item: SysUserSummary) => { const ok = await confirm({ title: isZh ? '删除用户' : 'Delete User', message: isZh ? `确认删除用户 ${item.display_name || item.username}?` : `Delete user ${item.display_name || item.username}?`, tone: 'warning', }); if (!ok) return; try { await axios.delete(`${APP_ENDPOINTS.apiBase}/sys/users/${item.id}`); notify(isZh ? '用户已删除。' : 'User deleted.', { tone: 'success' }); await loadAll(); } catch (error: any) { notify(error?.response?.data?.detail || (isZh ? '删除用户失败。' : 'Failed to delete user.'), { tone: 'error' }); } }; return (
setSearch(event.target.value)} placeholder={isZh ? '搜索用户名、显示名或角色...' : 'Search username, display name, or role...'} aria-label={isZh ? '搜索用户' : 'Search users'} />
{pagedItems.length === 0 ? ( ) : ( pagedItems.map((item) => { const busy = togglingUserId === item.id; return ( ); }) )}
{isZh ? '用户' : 'User'} {isZh ? '角色' : 'Role'} {isZh ? 'Bot 绑定' : 'Bot Binding'} {isZh ? '最近登录' : 'Last Login'} {isZh ? '状态' : 'Status'} {isZh ? '操作' : 'Actions'}
{filteredItems.length === 0 ? (isZh ? '当前没有可展示的用户。' : 'No users available right now.') : (isZh ? '当前页没有用户。' : 'No users on this page.')}
{item.display_name || item.username} {item.username}
{item.role?.name || (isZh ? '未分配角色' : 'No role')} {item.role?.role_key || '-'}
{isZh ? `已绑定 ${item.bot_ids?.length || 0} 个` : `${item.bot_ids?.length || 0} bound`} {(item.bot_ids || []).slice(0, 2).join(', ') || '-'}
{formatLastLogin(item.last_login_at, isZh)}
{item.is_active ? (isZh ? '启用' : 'Active') : (isZh ? '停用' : 'Disabled')}
openEdit(item)} tooltip={isZh ? '编辑用户' : 'Edit user'} aria-label={isZh ? '编辑用户' : 'Edit user'} > void handleDelete(item)} tooltip={isZh ? '删除用户' : 'Delete user'} aria-label={isZh ? '删除用户' : 'Delete user'} >
{isZh ? `第 ${currentPage} / ${pageCount} 页,共 ${filteredItems.length} 个用户` : `Page ${currentPage} / ${pageCount}, ${filteredItems.length} users`}
setPage((value) => Math.max(1, value - 1))} tooltip={isZh ? '上一页' : 'Previous'} aria-label={isZh ? '上一页' : 'Previous'} > = pageCount} onClick={() => setPage((value) => Math.min(pageCount, value + 1))} tooltip={isZh ? '下一页' : 'Next'} aria-label={isZh ? '下一页' : 'Next'} >
setEditorOpen(false)} bodyClassName="admin-management-drawer-body" closeLabel={isZh ? '关闭面板' : 'Close panel'} footer={(
{editingUserId == null ? (isZh ? '创建后可继续在列表中修改密码、角色和启停状态。' : 'After creation, password, role, and status can be updated from the list.') : (isZh ? '留空密码表示保持原密码不变。' : 'Leave password empty to keep the existing password.')}
)} >
{isZh ? '账户信息' : 'Account Details'}
); }