/** * 用途:批量导入用户弹窗(本地 CSV 导入 / 导入到 LDAP / 从 LDAP 导入 三个 Tab,含新建 OU 子弹窗) * 必需 props:onClose(关闭)、onImported(导入有成功项后回调,页面负责关窗并刷新列表) * 已知限制:CSV 预览仅展示前 50(本地)/ 30(到 LDAP)行;拼音用户名重复时以 username+序号作稳定 key */ import { useState, useEffect, useRef } from 'react'; import { Upload, X, RotateCcw, Plus, Users as UsersIcon, Server } from 'lucide-react'; import { userApi, ldapApi } from '@/api/user'; import { nameToUsername } from '@/utils/pinyin'; import { toast } from '@/hooks/useToast'; import { ModalOverlay } from '@/components/ModalOverlay'; interface BatchImportModalProps { onClose: () => void; onImported: () => void; } export function BatchImportModal({ onClose, onImported }: BatchImportModalProps) { const [tab, setTab] = useState<'local' | 'to-ldap' | 'from-ldap'>('local'); const [preview, setPreview] = useState([]); const [skipExisting, setSkipExisting] = useState(true); const [importing, setImporting] = useState(false); const [result, setResult] = useState(null); const fileRef = useRef(null); // LDAP state(ldapEnabled:后端已保存 LDAP 配置才显示相关 Tab) const [ldapEnabled, setLdapEnabled] = useState(false); const [ldapOus, setLdapOus] = useState([]); const [ldapUsers, setLdapUsers] = useState([]); const [selectedLdapUsers, setSelectedLdapUsers] = useState>(new Set()); const [ldapLoading, setLdapLoading] = useState(false); const [targetOu, setTargetOu] = useState(''); const [ldapReady, setLdapReady] = useState(false); // Create OU dialog const [showCreateOu, setShowCreateOu] = useState(false); const [newOuName, setNewOuName] = useState(''); const [newOuDesc, setNewOuDesc] = useState(''); const [creatingOu, setCreatingOu] = useState(false); // 挂载即探测配置(决定是否显示 LDAP Tab) useEffect(() => { ldapApi .getConfig() .then((res: any) => { setLdapEnabled(!!res?.ldap_host); }) .catch(() => setLdapEnabled(false)); }, []); // Check LDAP readiness + load OUs useEffect(() => { if (tab === 'to-ldap' || tab === 'from-ldap') { setLdapLoading(true); ldapApi .getConfig() .then((res: any) => { const ready = !!res?.ldap_host; setLdapReady(ready); if (ready && ldapOus.length === 0) { return loadLdapOus(); } setLdapLoading(false); }) .catch(() => { setLdapReady(false); setLdapLoading(false); }); } }, [tab]); // CSV parse const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const text = reader.result as string; const lines = text.split(/\r?\n/).filter((l) => l.trim()); if (lines.length < 2) { toast('error', '文件为空'); return; } const header = lines[0].split(',').map((h) => h.trim().replace(/"/g, '')); const nameIdx = header.findIndex((h) => /姓名|name|昵称|显示名/.test(h)); const emailIdx = header.findIndex((h) => /邮箱|email|mail/.test(h)); const roleIdx = header.findIndex((h) => /角色|role/.test(h)); if (nameIdx < 0) { toast('error', 'CSV 需要包含"姓名"列'); return; } const rows = lines .slice(1) .map((line) => { const cols = line.split(',').map((c) => c.trim().replace(/^"|"$/g, '')); const name = cols[nameIdx] || ''; return { display_name: name, username: nameToUsername(name), password: nameToUsername(name), email: emailIdx >= 0 ? cols[emailIdx] : '', role: roleIdx >= 0 ? cols[roleIdx] || 'developer' : 'developer', }; }) .filter((r) => r.display_name); setPreview(rows); setResult(null); }; reader.readAsText(file); e.target.value = ''; }; // LDAP OU loading const loadLdapOus = async () => { setLdapLoading(true); try { const res = (await ldapApi.listOus()) as any; setLdapOus(Array.isArray(res) ? res : res?.ous || res?.data || []); } catch { toast('error', '加载 LDAP OU 失败'); } finally { setLdapLoading(false); } }; const loadLdapUsers = async (baseDn: string) => { setLdapLoading(true); try { const res = (await ldapApi.listUsers({ base_dn: baseDn })) as any; setLdapUsers(Array.isArray(res) ? res : res?.users || res?.data || []); } catch { toast('error', '加载 LDAP 用户失败'); } finally { setLdapLoading(false); } }; // Create OU const handleCreateOu = async () => { if (!newOuName.trim()) { toast('warning', '请输入 OU 名称'); return; } setCreatingOu(true); try { await ldapApi.createOu({ ou_name: newOuName.trim(), description: newOuDesc.trim() }); toast('success', `OU "${newOuName}" 创建成功`); setShowCreateOu(false); setNewOuName(''); setNewOuDesc(''); await loadLdapOus(); } catch (e: any) { toast('error', e?.response?.data?.detail || '创建 OU 失败'); } finally { setCreatingOu(false); } }; // Import const handleImport = async () => { if (tab === 'local' && preview.length === 0) { toast('warning', '请先上传文件'); return; } if (tab === 'to-ldap' && preview.length === 0) { toast('warning', '请先上传文件'); return; } if (tab === 'to-ldap' && !targetOu) { toast('warning', '请选择目标 OU'); return; } if (tab === 'from-ldap' && selectedLdapUsers.size === 0) { toast('warning', '请选择用户'); return; } setImporting(true); try { let res: any; if (tab === 'local') { res = await userApi.batchImport({ users: preview, import_type: 'local', skip_existing: skipExisting }); } else if (tab === 'to-ldap') { res = await ldapApi.batchImportToLdap({ users: preview, ou_dn: targetOu }); } else { const users = ldapUsers.filter((u) => selectedLdapUsers.has(u.dn)); res = await userApi.batchImportLdap({ users }); } setResult(res); if (res.success_count > 0 || res.success) onImported(); if (tab === 'to-ldap') { toast('success', res.message || `成功 ${res.success_count} 个`); } else { toast('success', `成功导入 ${res.success_count} 个用户`); } } catch { toast('error', '导入失败'); } finally { setImporting(false); } }; const canImport = () => { if (importing) return false; if (tab === 'local') return preview.length > 0; if (tab === 'to-ldap') return preview.length > 0 && !!targetOu; return selectedLdapUsers.size > 0; }; // LDAP 未配置时隐藏 LDAP 相关 Tab(本地 CSV 始终可用) // LDAP 仅作只读接入:不再提供「导入到 LDAP」「从 LDAP 导入」 const TABS = [ { key: 'local' as const, label: '本地导入', icon: Upload }, ]; return (
{/* Header */}

批量导入用户

{/* Tabs */}
{TABS.map((t) => { const Icon = t.icon; return ( ); })}
{/* Content */}
{/* ── Local Import ── */} {tab === 'local' && ( <>
{preview.length > 0 && (
{preview.slice(0, 50).map((r, i) => ( ))} {preview.length > 50 && ( )}
姓名 用户名 邮箱 角色
{r.display_name} {r.username} {r.email || '-'} {r.role}
... 共 {preview.length} 条
)} )}
{/* Footer */}
); }