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,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -25,19 +17,14 @@ import {
|
|||||||
Network,
|
Network,
|
||||||
Mail,
|
Mail,
|
||||||
Phone,
|
Phone,
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Eye,
|
Eye,
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
Download,
|
|
||||||
Key,
|
|
||||||
Loader2,
|
|
||||||
RefreshCw,
|
|
||||||
User as UserIcon,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { userApi } from '@/services/api';
|
|
||||||
import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
||||||
|
import { UserDetailDialog } from './UserDetailDialog';
|
||||||
|
import { CsvImportDialog } from './CsvImportDialog';
|
||||||
|
import { LdapImportDialog } from './LdapImportDialog';
|
||||||
|
|
||||||
interface SystemUserTabProps {
|
interface SystemUserTabProps {
|
||||||
// Search
|
// Search
|
||||||
@@ -272,83 +259,14 @@ export function SystemUserTab({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* User Detail Dialog */}
|
{/* 用户详情 / 创建编辑 / CSV 导入 / LDAP 导入 弹窗 — 均由独立子组件承载 */}
|
||||||
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
|
<UserDetailDialog
|
||||||
<DialogContent className="bg-slate-900 border-slate-800">
|
selectedUser={selectedUser}
|
||||||
<DialogHeader>
|
setSelectedUser={setSelectedUser}
|
||||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
getUserStatusBadge={getUserStatusBadge}
|
||||||
<UserIcon className="h-5 w-5 text-blue-400" />
|
openEdit={openEdit}
|
||||||
{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>
|
|
||||||
|
|
||||||
{/* Create / Edit User Dialog */}
|
|
||||||
<UserFormDialog
|
<UserFormDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onOpenChange={setDialogOpen}
|
onOpenChange={setDialogOpen}
|
||||||
@@ -359,230 +277,35 @@ export function SystemUserTab({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* CSV 批量导入 Dialog */}
|
<CsvImportDialog
|
||||||
<Dialog open={importDialogOpen === 'csv'} onOpenChange={(v) => !v && setImportDialogOpen(null)}>
|
importDialogOpen={importDialogOpen}
|
||||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-2xl">
|
setImportDialogOpen={setImportDialogOpen}
|
||||||
<DialogHeader>
|
importPreview={importPreview}
|
||||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
setImportPreview={setImportPreview}
|
||||||
<Upload className="h-5 w-5 text-blue-400" />
|
importing={importing}
|
||||||
批量导入用户(CSV)
|
importResult={importResult}
|
||||||
</DialogTitle>
|
setImportResult={setImportResult}
|
||||||
<DialogDescription className="text-slate-500">
|
handleCsvUpload={handleCsvUpload}
|
||||||
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
|
handleBatchImport={handleBatchImport}
|
||||||
</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());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</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>
|
<LdapImportDialog
|
||||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => { setImportDialogOpen(null); setLdapUsers([]); }}>
|
importDialogOpen={importDialogOpen}
|
||||||
取消
|
setImportDialogOpen={setImportDialogOpen}
|
||||||
</Button>
|
ldapUsers={ldapUsers}
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleLdapImport} disabled={importing || ldapSelected.size === 0}>
|
setLdapUsers={setLdapUsers}
|
||||||
{importing ? '导入中...' : `导入选中 (${ldapSelected.size})`}
|
ldapSelected={ldapSelected}
|
||||||
</Button>
|
setLdapSelected={setLdapSelected}
|
||||||
</DialogFooter>
|
ldapLoading={ldapLoading}
|
||||||
</>
|
ldapImportOUs={ldapImportOUs}
|
||||||
) : (
|
ldapImportOUDN={ldapImportOUDN}
|
||||||
<div className="space-y-4 py-2">
|
setLdapImportOUDN={setLdapImportOUDN}
|
||||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${importResult.code === 200 ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
loadLdapUsers={loadLdapUsers}
|
||||||
{importResult.code === 200 ? (
|
handleLdapImport={handleLdapImport}
|
||||||
<CheckCircle className="h-6 w-6 text-emerald-400 shrink-0" />
|
importing={importing}
|
||||||
) : (
|
importResult={importResult}
|
||||||
<XCircle className="h-6 w-6 text-red-400 shrink-0" />
|
setImportResult={setImportResult}
|
||||||
)}
|
/>
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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