批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换: - 翻译词典: 22 namespaces, 3600+ keys - 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy) - 语言切换器在 Header 中可用 (🇨🇳/🇺🇸) - tsc 零错误, Vite 构建成功
385 lines
15 KiB
TypeScript
385 lines
15 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Search,
|
|
Plus,
|
|
Upload,
|
|
Network,
|
|
Mail,
|
|
Phone,
|
|
Eye,
|
|
Edit,
|
|
Trash2,
|
|
} from 'lucide-react';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { UserFormDialog, type UserFormData } from './UserFormDialog';
|
|
import { UserDetailDialog } from './UserDetailDialog';
|
|
import { CsvImportDialog } from './CsvImportDialog';
|
|
import { LdapImportDialog } from './LdapImportDialog';
|
|
import { usePermission } from '@/hooks/usePermission';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface SystemUserTabProps {
|
|
// Search
|
|
searchQuery: string;
|
|
setSearchQuery: (v: string) => void;
|
|
// Users
|
|
filteredUsers: any[];
|
|
loading: boolean;
|
|
selectedUser: any | null;
|
|
setSelectedUser: (u: any | null) => void;
|
|
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
|
|
// Create / Edit
|
|
dialogOpen: boolean;
|
|
setDialogOpen: (open: boolean) => void;
|
|
editingUser: any | null;
|
|
form: UserFormData;
|
|
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
|
|
saving: boolean;
|
|
handleSubmit: () => void;
|
|
openCreate: () => void;
|
|
openEdit: ( user: any) => void;
|
|
handleDelete: ( user: any) => void;
|
|
handleBatchDelete: () => void;
|
|
selectedUserIds: number[];
|
|
setSelectedUserIds: React.Dispatch<React.SetStateAction<number[]>>;
|
|
onResetPassword?: (user: any) => void;
|
|
// CSV import
|
|
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;
|
|
// LDAP import
|
|
ldapUsers: any[];
|
|
ldapSelected: Set<string>;
|
|
setLdapSelected: (s: Set<string>) => void;
|
|
ldapLoading: boolean;
|
|
ldapImportOUs: any[];
|
|
ldapImportOUDN: string;
|
|
setLdapImportOUDN: (v: string) => void;
|
|
loadLdapUsers: (ouDn?: string) => void;
|
|
handleLdapImport: () => void;
|
|
setLdapUsers: ( u: any[]) => void;
|
|
}
|
|
|
|
export function SystemUserTab({
|
|
searchQuery,
|
|
setSearchQuery,
|
|
filteredUsers,
|
|
loading,
|
|
selectedUser,
|
|
setSelectedUser,
|
|
getUserStatusBadge,
|
|
dialogOpen,
|
|
setDialogOpen,
|
|
editingUser,
|
|
form,
|
|
setForm,
|
|
saving,
|
|
handleSubmit,
|
|
openCreate,
|
|
openEdit,
|
|
handleDelete,
|
|
handleBatchDelete,
|
|
selectedUserIds,
|
|
setSelectedUserIds,
|
|
onResetPassword,
|
|
importDialogOpen,
|
|
setImportDialogOpen,
|
|
importPreview,
|
|
setImportPreview,
|
|
importing,
|
|
importResult,
|
|
setImportResult,
|
|
handleCsvUpload,
|
|
handleBatchImport,
|
|
ldapUsers,
|
|
ldapSelected,
|
|
setLdapSelected,
|
|
ldapLoading,
|
|
ldapImportOUs,
|
|
ldapImportOUDN,
|
|
setLdapImportOUDN,
|
|
loadLdapUsers,
|
|
handleLdapImport,
|
|
setLdapUsers,
|
|
}: SystemUserTabProps) {
|
|
const { t } = useTranslation();
|
|
const { hasPermission } = usePermission();
|
|
const canManage = hasPermission('system:user');
|
|
|
|
const toggleSelect = (id: number) => {
|
|
setSelectedUserIds((prev) =>
|
|
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id],
|
|
);
|
|
};
|
|
|
|
const allSelected = filteredUsers.length > 0 && selectedUserIds.length === filteredUsers.length;
|
|
const toggleSelectAll = () => {
|
|
setSelectedUserIds(allSelected ? [] : filteredUsers.map((u) => u.id));
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Filters */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="relative w-64">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
|
|
<Input
|
|
placeholder={t('system.searchUserDot')}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)]/50 border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{hasPermission('system:user') && (
|
|
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]" onClick={() => setImportDialogOpen('csv')}>
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
{t('common.batchImport')}
|
|
</Button>
|
|
)}
|
|
{hasPermission('system:user') && (
|
|
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]" onClick={() => setImportDialogOpen('ldap')}>
|
|
<Network className="h-4 w-4 mr-2" />
|
|
{t('system.ldapImport')}
|
|
</Button>
|
|
)}
|
|
{hasPermission('system:user') && (
|
|
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white" onClick={openCreate}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
新建用户
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{canManage && selectedUserIds.length > 0 && (
|
|
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
|
|
<span className="text-sm dg-text-secondary">
|
|
已选择 {selectedUserIds.length} 项 / 共 {filteredUsers.length} 项
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedUserIds([])}>
|
|
{t('cloud.clearSelection')}
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="dg-btn-danger dg-btn-sm" onClick={handleBatchDelete}>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
{t('system.terminalBatchDelete')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Users Table */}
|
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<CardContent className="p-0">
|
|
<div className="border-t border-[var(--dg-border)]">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
|
{canManage && (
|
|
<TableHead className="w-10 text-[var(--dg-text-secondary)]">
|
|
<Checkbox
|
|
checked={allSelected}
|
|
onCheckedChange={toggleSelectAll}
|
|
className="border-[var(--dg-border)]"
|
|
/>
|
|
</TableHead>
|
|
)}
|
|
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userTitleField')}</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.contactInfo')}</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userRole')}</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.source')}</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.userStatus')}</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)]">{t('system.lastLogin')}</TableHead>
|
|
<TableHead className="text-[var(--dg-text-secondary)] text-right">{t('system.auditAction')}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow className="border-[var(--dg-border)]">
|
|
<TableCell colSpan={canManage ? 8 : 7} className="text-center py-12">
|
|
<div className="flex items-center justify-center">
|
|
<div className="h-6 w-6 border-2 border-[var(--dg-border)] border-t-[var(--dg-blue)] rounded-full animate-spin" />
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : filteredUsers.length === 0 ? (
|
|
<TableRow className="border-[var(--dg-border)]">
|
|
<TableCell colSpan={canManage ? 8 : 7} className="text-center py-12 text-sm text-[var(--dg-text-muted)]">
|
|
暂无用户数据
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredUsers.map((user) => (
|
|
<TableRow key={user.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]/50">
|
|
{canManage && (
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedUserIds.includes(user.id)}
|
|
onCheckedChange={() => toggleSelect(user.id)}
|
|
className="border-[var(--dg-border)]"
|
|
/>
|
|
</TableCell>
|
|
)}
|
|
<TableCell>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-[var(--dg-blue)] flex items-center justify-center text-white text-sm font-medium">
|
|
{(user.nickname || user.username || '?').charAt(0)}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">{user.nickname || user.username}</p>
|
|
<p className="text-xs text-[var(--dg-text-muted)]">@{user.username}</p>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-1 text-sm text-[var(--dg-text-secondary)]">
|
|
<Mail className="h-3.5 w-3.5" />
|
|
{user.email || '--'}
|
|
</div>
|
|
{user.phone && (
|
|
<div className="flex items-center gap-1 text-sm text-[var(--dg-text-secondary)]">
|
|
<Phone className="h-3.5 w-3.5" />
|
|
{user.phone}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-wrap gap-1">
|
|
{(user.roles || []).map((role: string) => (
|
|
<Badge key={role} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
|
{role}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
{user.authSource === 'ldap' ? (
|
|
<Badge variant="outline" className="border-[var(--dg-cyan)]/30 text-[var(--dg-cyan)] bg-[var(--dg-cyan)]/10">
|
|
LDAP
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="outline" className="border-[var(--dg-text-muted)]/30 text-[var(--dg-text-muted)] bg-[var(--dg-text-muted)]/10">
|
|
本地
|
|
</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>{getUserStatusBadge(user.status)}</TableCell>
|
|
<TableCell>
|
|
<span className="text-sm text-[var(--dg-text-secondary)]">
|
|
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '--'}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex items-center justify-end gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
|
onClick={() => setSelectedUser(user)}
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
{hasPermission('system:user') && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
|
|
onClick={() => openEdit(user)}
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
{hasPermission('system:user') && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-red)]"
|
|
onClick={() => handleDelete(user)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 用户详情 / 创建编辑 / CSV 导入 / LDAP 导入 弹窗 — 均由独立子组件承载 */}
|
|
<UserDetailDialog
|
|
selectedUser={selectedUser}
|
|
setSelectedUser={setSelectedUser}
|
|
getUserStatusBadge={getUserStatusBadge}
|
|
openEdit={openEdit}
|
|
onResetPassword={onResetPassword}
|
|
/>
|
|
|
|
<UserFormDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
editingUser={editingUser}
|
|
form={form}
|
|
setForm={setForm}
|
|
saving={saving}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
|
|
<CsvImportDialog
|
|
importDialogOpen={importDialogOpen}
|
|
setImportDialogOpen={setImportDialogOpen}
|
|
importPreview={importPreview}
|
|
setImportPreview={setImportPreview}
|
|
importing={importing}
|
|
importResult={importResult}
|
|
setImportResult={setImportResult}
|
|
handleCsvUpload={handleCsvUpload}
|
|
handleBatchImport={handleBatchImport}
|
|
/>
|
|
|
|
<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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|