refactor: 拆分SystemUserTab.tsx(588→311行)提取UserDetailDialog+CsvImportDialog+LdapImportDialog子组件
This commit is contained in:
1 parent
367232f2b3
commit
2b5cbfb992
4 files changed
+469
-314
No files matched your search
@@ -0,0 +1,146 @@
|
||||
// CSV 批量导入用户弹窗(下载模板 + 文件选择 + 预览 + 导入结果) // Logan.Li
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Upload, Download, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { userApi } from '@/services/api';
|
||||
|
||||
interface CsvImportDialogProps {
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
|
||||
importPreview: any[];
|
||||
setImportPreview: (p: any[]) => void;
|
||||
importing: boolean;
|
||||
importResult: any;
|
||||
setImportResult: (r: any) => void;
|
||||
handleCsvUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleBatchImport: () => void;
|
||||
}
|
||||
|
||||
// CsvImportDialog 展示 CSV 导入流程:上传 → 预览 → 确认 → 结果 // Logan.Li
|
||||
export function CsvImportDialog({
|
||||
importDialogOpen,
|
||||
setImportDialogOpen,
|
||||
importPreview,
|
||||
setImportPreview,
|
||||
importing,
|
||||
importResult,
|
||||
setImportResult,
|
||||
handleCsvUpload,
|
||||
handleBatchImport,
|
||||
}: CsvImportDialogProps) {
|
||||
return (
|
||||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Upload className="h-5 w-5 text-blue-400" />
|
||||
批量导入用户(CSV)
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!importResult ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => userApi.downloadTemplate()}>
|
||||
<Download className="h-4 w-4 mr-1.5" />
|
||||
下载模板
|
||||
</Button>
|
||||
<label className="inline-flex items-center px-3 py-1.5 rounded-md text-sm bg-slate-800 border border-slate-700 text-slate-200 cursor-pointer hover:bg-slate-700 transition-colors">
|
||||
<Upload className="h-4 w-4 mr-1.5" />
|
||||
选择 CSV 文件
|
||||
<input type="file" accept=".csv" className="hidden" onChange={handleCsvUpload} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{importPreview.length > 0 && (
|
||||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-slate-800/50 text-xs text-slate-400">
|
||||
预览:{importPreview.length} 条数据
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800">
|
||||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">昵称</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">手机</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importPreview.slice(0, 5).map((u, i) => (
|
||||
<TableRow key={i} className="border-slate-800">
|
||||
<TableCell className="text-slate-200 text-xs">{u.username}</TableCell>
|
||||
<TableCell className="text-slate-300 text-xs">{u.nickname}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.email}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.phone}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{importPreview.length > 5 && (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={4} className="text-slate-500 text-xs text-center">
|
||||
还有 {importPreview.length - 5} 条...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleBatchImport} disabled={importing || importPreview.length === 0}>
|
||||
{importing ? '导入中...' : `确认导入 (${importPreview.length})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||
{importResult.code === 200 ? (
|
||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
</div>
|
||||
{importResult.errors?.length > 0 && (
|
||||
<div className="max-h-32 overflow-auto rounded-lg bg-slate-800/50 p-3 space-y-1">
|
||||
{importResult.errors.map((err: string, i: number) => (
|
||||
<div key={i} className="text-xs text-red-400">{err}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setImportPreview([]); setImportResult(null); }}>
|
||||
完成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// LDAP 用户导入弹窗(OU 选择 + 用户列表多选 + 导入结果) // Logan.Li
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Network, Loader2, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
interface LdapImportDialogProps {
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
|
||||
ldapUsers: any[];
|
||||
setLdapUsers: (u: any[]) => void;
|
||||
ldapSelected: Set<string>;
|
||||
setLdapSelected: (s: Set<string>) => void;
|
||||
ldapLoading: boolean;
|
||||
ldapImportOUs: any[];
|
||||
ldapImportOUDN: string;
|
||||
setLdapImportOUDN: (v: string) => void;
|
||||
loadLdapUsers: (ouDn?: string) => void;
|
||||
handleLdapImport: () => void;
|
||||
importing: boolean;
|
||||
importResult: any;
|
||||
setImportResult: (r: any) => void;
|
||||
}
|
||||
|
||||
// LdapImportDialog 展示 LDAP 用户检索和多选导入流程 // Logan.Li
|
||||
export function LdapImportDialog({
|
||||
importDialogOpen,
|
||||
setImportDialogOpen,
|
||||
ldapUsers,
|
||||
setLdapUsers,
|
||||
ldapSelected,
|
||||
setLdapSelected,
|
||||
ldapLoading,
|
||||
ldapImportOUs,
|
||||
ldapImportOUDN,
|
||||
setLdapImportOUDN,
|
||||
loadLdapUsers,
|
||||
handleLdapImport,
|
||||
importing,
|
||||
importResult,
|
||||
setImportResult,
|
||||
}: LdapImportDialogProps) {
|
||||
return (
|
||||
<Dialog open={importDialogOpen === 'ldap'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-cyan-400" />
|
||||
从 LDAP 导入用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
从 LDAP 服务器检索用户列表,选择需要导入的用户
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!importResult ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 whitespace-nowrap">选择 OU</span>
|
||||
<select
|
||||
className="bg-slate-800 border border-slate-700 text-slate-200 text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={ldapImportOUDN}
|
||||
onChange={(e) => {
|
||||
setLdapImportOUDN(e.target.value);
|
||||
loadLdapUsers(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">根 (全部)</option>
|
||||
{ldapImportOUs.map((ou) => (
|
||||
<option key={ou.dn} value={ou.dn}>{ou.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 ml-auto" onClick={() => loadLdapUsers(ldapImportOUDN)} disabled={ldapLoading}>
|
||||
{ldapLoading ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1.5" />}
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
|
||||
</p>
|
||||
|
||||
{ldapUsers.length > 0 && (
|
||||
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 sticky top-0 bg-slate-900">
|
||||
<TableHead className="w-10 text-slate-400 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
checked={ldapSelected.size === ldapUsers.length && ldapUsers.length > 0}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setLdapSelected(new Set(ldapUsers.map((u) => u.dn)));
|
||||
} else {
|
||||
setLdapSelected(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">显示名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">DN</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.map((u) => (
|
||||
<TableRow key={u.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
checked={ldapSelected.has(u.dn)}
|
||||
onChange={(e) => {
|
||||
const next = new Set(ldapSelected);
|
||||
if (e.target.checked) next.add(u.dn);
|
||||
else next.delete(u.dn);
|
||||
setLdapSelected(next);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-200 text-xs">{u.uid}</TableCell>
|
||||
<TableCell className="text-slate-300 text-xs">{u.cn}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.mail}</TableCell>
|
||||
<TableCell className="text-slate-500 text-xs font-mono truncate max-w-[200px]">{u.dn}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
||||
{importing ? '导入中...' : `导入选中 (${ldapSelected.size})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||
{importResult.code === 200 ? (
|
||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); setImportResult(null); }}>
|
||||
完成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -10,14 +10,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
@@ -25,19 +17,14 @@ import {
|
||||
Network,
|
||||
Mail,
|
||||
Phone,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
Download,
|
||||
Key,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
User as UserIcon,
|
||||
} from 'lucide-react';
|
||||
import { userApi } from '@/services/api';
|
||||
import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
||||
import { UserDetailDialog } from './UserDetailDialog';
|
||||
import { CsvImportDialog } from './CsvImportDialog';
|
||||
import { LdapImportDialog } from './LdapImportDialog';
|
||||
|
||||
interface SystemUserTabProps {
|
||||
// Search
|
||||
@@ -272,83 +259,14 @@ export function SystemUserTab({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* User Detail Dialog */}
|
||||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<UserIcon className="h-5 w-5 text-blue-400" />
|
||||
{selectedUser?.nickname}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
@{selectedUser?.username} · {selectedUser?.email}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* 用户详情 / 创建编辑 / CSV 导入 / LDAP 导入 弹窗 — 均由独立子组件承载 */}
|
||||
<UserDetailDialog
|
||||
selectedUser={selectedUser}
|
||||
setSelectedUser={setSelectedUser}
|
||||
getUserStatusBadge={getUserStatusBadge}
|
||||
openEdit={openEdit}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">用户名</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.username}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">昵称</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.nickname}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">邮箱</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">手机号</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.phone || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">状态</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser && getUserStatusBadge(selectedUser.status)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">创建时间</p>
|
||||
<p className="text-sm text-slate-200">
|
||||
{selectedUser && selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString() : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">角色</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedUser?.roles.map((role: string) => (
|
||||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||||
{role}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300">
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
重置密码
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => {
|
||||
if (selectedUser) {
|
||||
setSelectedUser(null);
|
||||
openEdit(selectedUser);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Create / Edit User Dialog */}
|
||||
<UserFormDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
@@ -359,230 +277,35 @@ export function SystemUserTab({
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
{/* CSV 批量导入 Dialog */}
|
||||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Upload className="h-5 w-5 text-blue-400" />
|
||||
批量导入用户(CSV)
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!importResult ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => userApi.downloadTemplate()}>
|
||||
<Download className="h-4 w-4 mr-1.5" />
|
||||
下载模板
|
||||
</Button>
|
||||
<label className="inline-flex items-center px-3 py-1.5 rounded-md text-sm bg-slate-800 border border-slate-700 text-slate-200 cursor-pointer hover:bg-slate-700 transition-colors">
|
||||
<Upload className="h-4 w-4 mr-1.5" />
|
||||
选择 CSV 文件
|
||||
<input type="file" accept=".csv" className="hidden" onChange={handleCsvUpload} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{importPreview.length > 0 && (
|
||||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-slate-800/50 text-xs text-slate-400">
|
||||
预览:{importPreview.length} 条数据
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800">
|
||||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">昵称</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">手机</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importPreview.slice(0, 5).map((u, i) => (
|
||||
<TableRow key={i} className="border-slate-800">
|
||||
<TableCell className="text-slate-200 text-xs">{u.username}</TableCell>
|
||||
<TableCell className="text-slate-300 text-xs">{u.nickname}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.email}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.phone}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{importPreview.length > 5 && (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={4} className="text-slate-500 text-xs text-center">
|
||||
还有 {importPreview.length - 5} 条...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setImportPreview([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleBatchImport} disabled={importing || importPreview.length === 0}>
|
||||
{importing ? '导入中...' : `确认导入 (${importPreview.length})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||
{importResult.code === 200 ? (
|
||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
</div>
|
||||
{importResult.errors?.length > 0 && (
|
||||
<div className="max-h-32 overflow-auto rounded-lg bg-slate-800/50 p-3 space-y-1">
|
||||
{importResult.errors.map((err: string, i: number) => (
|
||||
<div key={i} className="text-xs text-red-400">{err}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setImportPreview([]); setImportResult(null); }}>
|
||||
完成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* LDAP 导入 Dialog */}
|
||||
<Dialog open={importDialogOpen === 'ldap'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-cyan-400" />
|
||||
从 LDAP 导入用户
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
从 LDAP 服务器检索用户列表,选择需要导入的用户
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{!importResult ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 whitespace-nowrap">选择 OU</span>
|
||||
<select
|
||||
className="bg-slate-800 border border-slate-700 text-slate-200 text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={ldapImportOUDN}
|
||||
onChange={(e) => {
|
||||
setLdapImportOUDN(e.target.value);
|
||||
loadLdapUsers(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">根 (全部)</option>
|
||||
{ldapImportOUs.map((ou) => (
|
||||
<option key={ou.dn} value={ou.dn}>{ou.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 ml-auto" onClick={() => loadLdapUsers(ldapImportOUDN)} disabled={ldapLoading}>
|
||||
{ldapLoading ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1.5" />}
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
|
||||
</p>
|
||||
|
||||
{ldapUsers.length > 0 && (
|
||||
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 sticky top-0 bg-slate-900">
|
||||
<TableHead className="w-10 text-slate-400 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
checked={ldapSelected.size === ldapUsers.length && ldapUsers.length > 0}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setLdapSelected(new Set(ldapUsers.map((u) => u.dn)));
|
||||
} else {
|
||||
setLdapSelected(new Set());
|
||||
}
|
||||
}}
|
||||
<CsvImportDialog
|
||||
importDialogOpen={importDialogOpen}
|
||||
setImportDialogOpen={setImportDialogOpen}
|
||||
importPreview={importPreview}
|
||||
setImportPreview={setImportPreview}
|
||||
importing={importing}
|
||||
importResult={importResult}
|
||||
setImportResult={setImportResult}
|
||||
handleCsvUpload={handleCsvUpload}
|
||||
handleBatchImport={handleBatchImport}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">用户名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">显示名</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">邮箱</TableHead>
|
||||
<TableHead className="text-slate-400 text-xs">DN</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ldapUsers.map((u) => (
|
||||
<TableRow key={u.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600"
|
||||
checked={ldapSelected.has(u.dn)}
|
||||
onChange={(e) => {
|
||||
const next = new Set(ldapSelected);
|
||||
if (e.target.checked) next.add(u.dn);
|
||||
else next.delete(u.dn);
|
||||
setLdapSelected(next);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-200 text-xs">{u.uid}</TableCell>
|
||||
<TableCell className="text-slate-300 text-xs">{u.cn}</TableCell>
|
||||
<TableCell className="text-slate-400 text-xs">{u.mail}</TableCell>
|
||||
<TableCell className="text-slate-500 text-xs font-mono truncate max-w-[200px]">{u.dn}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
||||
{importing ? '导入中...' : `导入选中 (${ldapSelected.size})`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||
{importResult.code === 200 ? (
|
||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-slate-200">{importResult.message}</span>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); setImportResult(null); }}>
|
||||
完成
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<LdapImportDialog
|
||||
importDialogOpen={importDialogOpen}
|
||||
setImportDialogOpen={setImportDialogOpen}
|
||||
ldapUsers={ldapUsers}
|
||||
setLdapUsers={setLdapUsers}
|
||||
ldapSelected={ldapSelected}
|
||||
setLdapSelected={setLdapSelected}
|
||||
ldapLoading={ldapLoading}
|
||||
ldapImportOUs={ldapImportOUs}
|
||||
ldapImportOUDN={ldapImportOUDN}
|
||||
setLdapImportOUDN={setLdapImportOUDN}
|
||||
loadLdapUsers={loadLdapUsers}
|
||||
handleLdapImport={handleLdapImport}
|
||||
importing={importing}
|
||||
importResult={importResult}
|
||||
setImportResult={setImportResult}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// 用户详情弹窗(只读信息 + 角色徽章 + 编辑入口) // Logan.Li
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Key, Edit, User as UserIcon } from 'lucide-react';
|
||||
|
||||
interface UserDetailDialogProps {
|
||||
selectedUser: any | null;
|
||||
setSelectedUser: (u: any | null) => void;
|
||||
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
|
||||
openEdit: (user: any) => void;
|
||||
}
|
||||
|
||||
// UserDetailDialog 仅展示选中用户的基本信息和角色列表 // Logan.Li
|
||||
export function UserDetailDialog({
|
||||
selectedUser,
|
||||
setSelectedUser,
|
||||
getUserStatusBadge,
|
||||
openEdit,
|
||||
}: UserDetailDialogProps) {
|
||||
return (
|
||||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<UserIcon className="h-5 w-5 text-blue-400" />
|
||||
{selectedUser?.nickname}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
@{selectedUser?.username} · {selectedUser?.email}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">用户名</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.username}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">昵称</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.nickname}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">邮箱</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">手机号</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser?.phone || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">状态</p>
|
||||
<p className="text-sm text-slate-200">{selectedUser && getUserStatusBadge(selectedUser.status)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">创建时间</p>
|
||||
<p className="text-sm text-slate-200">
|
||||
{selectedUser && selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString() : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">角色</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedUser?.roles.map((role: string) => (
|
||||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||||
{role}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300">
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
重置密码
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => {
|
||||
if (selectedUser) {
|
||||
setSelectedUser(null);
|
||||
openEdit(selectedUser);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user