319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
import { useState, useEffect } from 'react';
|
||
import { useLocation } from 'react-router-dom';
|
||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { CheckCircle, XCircle, Users, Shield, Settings } from 'lucide-react';
|
||
import { userApi } from '@/api';
|
||
import type { Role } from '@/types';
|
||
import { SystemUserTab } from './SystemUserTab';
|
||
import { SystemRolesTab } from './SystemRolesTab';
|
||
import { SystemSettingsTab } from './SystemSettingsTab';
|
||
import { ResetPasswordDialog } from './ResetPasswordDialog';
|
||
import { useLdapManagement } from './useLdapManagement';
|
||
import type { UserFormData } from './UserFormDialog';
|
||
|
||
// 后端用户数据转换为前端兼容格式
|
||
function mapUser( u: any) {
|
||
return {
|
||
...u,
|
||
status: u.status === 1 || u.status === 'active' ? ('active' as const) : ('inactive' as const),
|
||
roles: Array.isArray(u.roles) ? u.roles : u.role ? [u.role] : [],
|
||
lastLoginAt: u.last_login ?? u.lastLoginAt,
|
||
createdAt: u.created_at ?? u.createdAt,
|
||
authSource: u.auth_source ?? u.authSource ?? 'local',
|
||
};
|
||
}
|
||
|
||
const emptyForm: UserFormData = {
|
||
username: '',
|
||
nickname: '',
|
||
email: '',
|
||
phone: '',
|
||
role: '',
|
||
password: '',
|
||
status: 1 as number,
|
||
};
|
||
|
||
export function SystemManager() {
|
||
const confirm = useConfirm();
|
||
const location = useLocation();
|
||
// 侧边栏菜单路径映射到 Tab // Logan.Li
|
||
const pathToTab: Record<string, string> = {
|
||
'/users': 'users',
|
||
'/roles': 'roles',
|
||
'/settings': 'settings',
|
||
};
|
||
const [activeTab, setActiveTab] = useState(pathToTab[location.pathname] || 'users');
|
||
|
||
// URL 变化时同步 Tab(侧边栏菜单点击触发) // Logan.Li
|
||
useEffect(() => {
|
||
const tab = pathToTab[location.pathname];
|
||
if (tab && tab !== activeTab) {
|
||
setActiveTab(tab);
|
||
}
|
||
}, [location.pathname]);
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [users, setUsers] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
||
const [resetPasswordUser, setResetPasswordUser] = useState<any | null>(null);
|
||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||
|
||
// 创建/编辑用户弹窗
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
const [editingUser, setEditingUser] = useState<any | null>(null);
|
||
const [saving, setSaving] = useState(false);
|
||
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
|
||
|
||
// CSV 批量导入弹窗
|
||
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
|
||
const [importPreview, setImportPreview] = useState<any[]>([]);
|
||
const [importResult, setImportResult] = useState<any>(null);
|
||
|
||
const loadUsers = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await userApi.getUsers({ page: 1, page_size: 100 });
|
||
const rawUsers = res?.list ?? [];
|
||
setUsers(rawUsers.map(mapUser));
|
||
} catch (error) {
|
||
console.error('Failed to fetch users:', error);
|
||
setUsers([]);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// LDAP 状态全部由 hook 管理(在 loadUsers 之后调用避免 TDZ) // Logan.Li
|
||
const ldap = useLdapManagement(loadUsers);
|
||
|
||
useEffect(() => {
|
||
loadUsers();
|
||
ldap.loadLdapConfig();
|
||
}, []);
|
||
|
||
// LDAP 导入 Dialog 打开时加载 OU 和用户
|
||
useEffect(() => {
|
||
if (importDialogOpen === 'ldap') {
|
||
ldap.loadImportDialogData();
|
||
}
|
||
}, [importDialogOpen]);
|
||
|
||
const filteredUsers = users.filter(
|
||
(u) =>
|
||
(u.username || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
(u.nickname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
(u.email || '').toLowerCase().includes(searchQuery.toLowerCase())
|
||
);
|
||
|
||
const getUserStatusBadge = (status: 'active' | 'inactive') => {
|
||
return status === 'active' ? (
|
||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||
<CheckCircle className="h-3 w-3 mr-1" />
|
||
正常
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="outline" className="border-slate-500/30 text-slate-400 bg-slate-500/10">
|
||
<XCircle className="h-3 w-3 mr-1" />
|
||
禁用
|
||
</Badge>
|
||
);
|
||
};
|
||
|
||
const openCreate = () => {
|
||
setEditingUser(null);
|
||
setForm({ ...emptyForm });
|
||
setDialogOpen(true);
|
||
};
|
||
|
||
const openEdit = ( user: any) => {
|
||
setEditingUser(user);
|
||
setForm({
|
||
username: user.username || '',
|
||
nickname: user.nickname || '',
|
||
email: user.email || '',
|
||
phone: user.phone || '',
|
||
role: Array.isArray(user.roles) ? user.roles[0] || '' : user.role || '',
|
||
password: '',
|
||
status: user.status === 'active' ? 1 : 0,
|
||
});
|
||
setDialogOpen(true);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
setSaving(true);
|
||
try {
|
||
const payload: Record<string, unknown> = {
|
||
username: form.username,
|
||
nickname: form.nickname,
|
||
email: form.email,
|
||
phone: form.phone,
|
||
role: form.role,
|
||
status: form.status,
|
||
};
|
||
if (form.password) {
|
||
payload.password = form.password;
|
||
}
|
||
if (editingUser) {
|
||
await userApi.updateUser(editingUser.id, payload);
|
||
} else {
|
||
await userApi.createUser(payload);
|
||
}
|
||
setDialogOpen(false);
|
||
await loadUsers();
|
||
} catch (error) {
|
||
console.error('Failed to save user:', error);
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async ( user: any) => {
|
||
const ok = await confirm({ title: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
|
||
if (!ok) return;
|
||
try {
|
||
await userApi.deleteUser(user.id);
|
||
await loadUsers();
|
||
} catch (error) {
|
||
console.error('Failed to delete user:', error);
|
||
}
|
||
};
|
||
|
||
// CSV 文件解析
|
||
const handleCsvUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
const text = ev.target?.result as string;
|
||
const lines = text.split('\n').filter((l) => l.trim() && !l.startsWith('\xEF\xBB\xBF'));
|
||
if (lines.length < 2) return;
|
||
// Find column indices from header
|
||
const headers = lines[0].split(',').map((h) => h.trim().toLowerCase());
|
||
const idxUsername = headers.findIndex((h) => h.includes('用户名') || h.includes('username'));
|
||
const idxNickname = headers.findIndex((h) => h.includes('昵称') || h.includes('name') || h.includes('显示名'));
|
||
const idxEmail = headers.findIndex((h) => h.includes('邮箱') || h.includes('email') || h.includes('mail'));
|
||
const idxPhone = headers.findIndex((h) => h.includes('手机') || h.includes('phone'));
|
||
const idxRole = headers.findIndex((h) => h.includes('角色') || h.includes('role'));
|
||
const idxPassword = headers.findIndex((h) => h.includes('密码') || h.includes('password'));
|
||
|
||
const parsed: Record<string, unknown>[] = [];
|
||
for (let i = 1; i < lines.length; i++) {
|
||
const cols = lines[i].split(',').map((c) => c.trim());
|
||
parsed.push({
|
||
username: idxUsername >= 0 ? cols[idxUsername] : '',
|
||
nickname: idxNickname >= 0 ? cols[idxNickname] : '',
|
||
email: idxEmail >= 0 ? cols[idxEmail] : '',
|
||
phone: idxPhone >= 0 ? cols[idxPhone] : '',
|
||
role: idxRole >= 0 ? cols[idxRole] : '',
|
||
password: idxPassword >= 0 ? cols[idxPassword] : '',
|
||
});
|
||
}
|
||
setImportPreview(parsed);
|
||
setImportResult(null);
|
||
};
|
||
reader.readAsText(file, 'UTF-8');
|
||
};
|
||
|
||
// 执行 CSV 批量导入
|
||
const handleBatchImport = async () => {
|
||
ldap.setImporting(true);
|
||
try {
|
||
const res = await userApi.batchImport({ users: importPreview, skip_existing: true });
|
||
setImportResult(res);
|
||
await loadUsers();
|
||
} catch (error) {
|
||
setImportResult({ code: 500, message: error?.message || '导入失败' });
|
||
} finally {
|
||
ldap.setImporting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Page Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-100">系统管理</h1>
|
||
<p className="text-sm text-slate-500 mt-1">用户、权限与系统配置</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||
<TabsList className="bg-slate-800 border border-slate-700">
|
||
<TabsTrigger value="users" className="data-[state=active]:bg-slate-700">
|
||
<Users className="h-4 w-4 mr-2" />
|
||
用户管理
|
||
</TabsTrigger>
|
||
<TabsTrigger value="roles" className="data-[state=active]:bg-slate-700">
|
||
<Shield className="h-4 w-4 mr-2" />
|
||
角色权限
|
||
</TabsTrigger>
|
||
<TabsTrigger value="settings" className="data-[state=active]:bg-slate-700">
|
||
<Settings className="h-4 w-4 mr-2" />
|
||
系统设置
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="users" className="mt-4 space-y-4">
|
||
<SystemUserTab
|
||
searchQuery={searchQuery}
|
||
setSearchQuery={setSearchQuery}
|
||
filteredUsers={filteredUsers}
|
||
loading={loading}
|
||
selectedUser={selectedUser}
|
||
setSelectedUser={setSelectedUser}
|
||
getUserStatusBadge={getUserStatusBadge}
|
||
dialogOpen={dialogOpen}
|
||
setDialogOpen={setDialogOpen}
|
||
editingUser={editingUser}
|
||
form={form}
|
||
setForm={setForm}
|
||
saving={saving}
|
||
handleSubmit={handleSubmit}
|
||
openCreate={openCreate}
|
||
openEdit={openEdit}
|
||
handleDelete={handleDelete}
|
||
onResetPassword={(u) => { setSelectedUser(null); setResetPasswordUser(u); }}
|
||
importDialogOpen={importDialogOpen}
|
||
setImportDialogOpen={setImportDialogOpen}
|
||
importPreview={importPreview}
|
||
setImportPreview={setImportPreview}
|
||
importing={ldap.importing}
|
||
importResult={importResult}
|
||
setImportResult={setImportResult}
|
||
handleCsvUpload={handleCsvUpload}
|
||
handleBatchImport={handleBatchImport}
|
||
ldapUsers={ldap.ldapUsers}
|
||
ldapSelected={ldap.ldapSelected}
|
||
setLdapSelected={ldap.setLdapSelected}
|
||
ldapLoading={ldap.ldapLoading}
|
||
ldapImportOUs={ldap.ldapImportOUs}
|
||
ldapImportOUDN={ldap.ldapImportOUDN}
|
||
setLdapImportOUDN={ldap.setLdapImportOUDN}
|
||
loadLdapUsers={ldap.loadLdapUsers}
|
||
handleLdapImport={ldap.handleLdapImport}
|
||
setLdapUsers={ldap.setLdapUsers}
|
||
/>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="roles" className="mt-4 space-y-4">
|
||
<SystemRolesTab
|
||
selectedRole={selectedRole}
|
||
setSelectedRole={setSelectedRole}
|
||
/>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="settings" className="mt-4">
|
||
<SystemSettingsTab {...ldap.settingsTabProps} />
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
<ResetPasswordDialog user={resetPasswordUser} onClose={() => setResetPasswordUser(null)} />
|
||
</div>
|
||
);
|
||
}
|