|
|
@@ -1,353 +1,297 @@
|
|
|
-import React, { useState } from 'react';
|
|
|
-import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown, Building2 } from 'lucide-react';
|
|
|
-
|
|
|
-interface TableRow {
|
|
|
- id: number;
|
|
|
- [key: string]: any;
|
|
|
-}
|
|
|
+import React, { useState, useEffect, useRef } from 'react';
|
|
|
+import { Plus, Search, Edit2, Trash2, MoreVertical, Upload, Download, Key, UserCog, Eye, RefreshCw } from 'lucide-react';
|
|
|
+import { userApi } from '../api/User';
|
|
|
+import { UserVO, WorkstationNode } from '../types';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import DeptTree from './user/DeptTree';
|
|
|
+import UserForm, { UserFormRef } from './user/UserForm';
|
|
|
+import UserImportForm, { UserImportFormRef } from './user/UserImportForm';
|
|
|
+import UserAssignRoleForm, { UserAssignRoleFormRef } from './user/UserAssignRoleForm';
|
|
|
+import FaceOrFingerForm, { FaceOrFingerFormRef } from './user/FaceOrFingerForm';
|
|
|
+import { Switch } from './ui/switch';
|
|
|
+import { Button } from './ui/button';
|
|
|
+import {
|
|
|
+ DropdownMenu,
|
|
|
+ DropdownMenuContent,
|
|
|
+ DropdownMenuItem,
|
|
|
+ DropdownMenuTrigger,
|
|
|
+} from './ui/dropdown-menu';
|
|
|
|
|
|
interface UserManagementProps {
|
|
|
subMenu: string;
|
|
|
}
|
|
|
|
|
|
export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
- const [searchTerm, setSearchTerm] = useState('');
|
|
|
- const [showAddModal, setShowAddModal] = useState(false);
|
|
|
- const [editingItem, setEditingItem] = useState<TableRow | null>(null);
|
|
|
- const [expandedDeptIds, setExpandedDeptIds] = useState<number[]>([1]); // 部门树展开的节点
|
|
|
- const [selectedDeptId, setSelectedDeptId] = useState<number | null>(null); // 选中的部门ID
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [list, setList] = useState<UserVO[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [queryParams, setQueryParams] = useState({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ username: '',
|
|
|
+ mobile: '',
|
|
|
+ status: undefined as number | undefined,
|
|
|
+ workstationId: undefined as number | undefined,
|
|
|
+ createTime: [] as string[],
|
|
|
+ });
|
|
|
+ const [exportLoading, setExportLoading] = useState(false);
|
|
|
|
|
|
- // 部门树形数据
|
|
|
- const departmentTreeData = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- name: '总公司',
|
|
|
- manager: '李总',
|
|
|
- memberCount: 120,
|
|
|
- children: [
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- name: '技术中心',
|
|
|
- manager: '张三',
|
|
|
- memberCount: 45,
|
|
|
- parentId: 1,
|
|
|
- children: [
|
|
|
- {
|
|
|
- id: 21,
|
|
|
- name: '研发部',
|
|
|
- manager: '王研',
|
|
|
- memberCount: 25,
|
|
|
- parentId: 2
|
|
|
- },
|
|
|
- {
|
|
|
- id: 22,
|
|
|
- name: '测试部',
|
|
|
- manager: '赵测',
|
|
|
- memberCount: 12,
|
|
|
- parentId: 2
|
|
|
- },
|
|
|
- {
|
|
|
- id: 23,
|
|
|
- name: '运维部',
|
|
|
- manager: '李运',
|
|
|
- memberCount: 8,
|
|
|
- parentId: 2
|
|
|
- }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- name: '市场中心',
|
|
|
- manager: '李四',
|
|
|
- memberCount: 30,
|
|
|
- parentId: 1,
|
|
|
- children: [
|
|
|
- {
|
|
|
- id: 31,
|
|
|
- name: '市场部',
|
|
|
- manager: '钱市',
|
|
|
- memberCount: 18,
|
|
|
- parentId: 3
|
|
|
- },
|
|
|
- {
|
|
|
- id: 32,
|
|
|
- name: '销售部',
|
|
|
- manager: '孙销',
|
|
|
- memberCount: 12,
|
|
|
- parentId: 3
|
|
|
- }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4,
|
|
|
- name: '行政中心',
|
|
|
- manager: '王五',
|
|
|
- memberCount: 25,
|
|
|
- parentId: 1,
|
|
|
- children: [
|
|
|
- {
|
|
|
- id: 41,
|
|
|
- name: '人力资源部',
|
|
|
- manager: '周人',
|
|
|
- memberCount: 10,
|
|
|
- parentId: 4
|
|
|
- },
|
|
|
- {
|
|
|
- id: 42,
|
|
|
- name: '财务部',
|
|
|
- manager: '吴财',
|
|
|
- memberCount: 8,
|
|
|
- parentId: 4
|
|
|
- },
|
|
|
- {
|
|
|
- id: 43,
|
|
|
- name: '行政部',
|
|
|
- manager: '郑行',
|
|
|
- memberCount: 7,
|
|
|
- parentId: 4
|
|
|
- }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- id: 5,
|
|
|
- name: '安全部',
|
|
|
- manager: '赵六',
|
|
|
- memberCount: 20,
|
|
|
- parentId: 1
|
|
|
- }
|
|
|
- ]
|
|
|
+ // 子组件引用
|
|
|
+ const formRef = useRef<UserFormRef>(null);
|
|
|
+ const importFormRef = useRef<UserImportFormRef>(null);
|
|
|
+ const assignRoleFormRef = useRef<UserAssignRoleFormRef>(null);
|
|
|
+ const faceOrFingerFormRef = useRef<FaceOrFingerFormRef>(null);
|
|
|
+
|
|
|
+ // 获取用户列表
|
|
|
+ const getList = async () => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const response = await userApi.getUserPage(queryParams);
|
|
|
+ setList(response.list || []);
|
|
|
+ setTotal(response.total || 0);
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '获取用户列表失败');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
}
|
|
|
- ];
|
|
|
+ };
|
|
|
|
|
|
- // 切换部门树展开/收起
|
|
|
- const toggleDeptExpand = (id: number) => {
|
|
|
- setExpandedDeptIds(prev =>
|
|
|
- prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
|
|
|
- );
|
|
|
+ useEffect(() => {
|
|
|
+ getList();
|
|
|
+ }, [queryParams.pageNo, queryParams.pageSize, queryParams.workstationId]);
|
|
|
+
|
|
|
+ // 搜索
|
|
|
+ const handleQuery = () => {
|
|
|
+ setQueryParams(prev => ({ ...prev, pageNo: 1 }));
|
|
|
+ getList();
|
|
|
};
|
|
|
|
|
|
- // 用户列表数据
|
|
|
- const userData: TableRow[] = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- username: 'admin',
|
|
|
- realName: '张三',
|
|
|
- phone: '13800138001',
|
|
|
- email: 'zhangsan@example.com',
|
|
|
- department: '技术部',
|
|
|
- role: '超级管理员',
|
|
|
- status: '在线',
|
|
|
- lastLogin: '2025-12-04 10:30',
|
|
|
- createTime: '2025-01-01'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- username: 'user001',
|
|
|
- realName: '李四',
|
|
|
- phone: '13800138002',
|
|
|
- email: 'lisi@example.com',
|
|
|
- department: '运维部',
|
|
|
- role: '部门管理员',
|
|
|
- status: '离线',
|
|
|
- lastLogin: '2025-12-03 18:45',
|
|
|
- createTime: '2025-01-05'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- username: 'user002',
|
|
|
- realName: '王五',
|
|
|
- phone: '13800138003',
|
|
|
- email: 'wangwu@example.com',
|
|
|
- department: '安全部',
|
|
|
- role: '操作员',
|
|
|
- status: '在线',
|
|
|
- lastLogin: '2025-12-04 09:15',
|
|
|
- createTime: '2025-01-10'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4,
|
|
|
- username: 'user003',
|
|
|
- realName: '赵六',
|
|
|
- phone: '13800138004',
|
|
|
- email: 'zhaoliu@example.com',
|
|
|
- department: '测试部',
|
|
|
- role: '审计员',
|
|
|
- status: '在线',
|
|
|
- lastLogin: '2025-12-04 11:00',
|
|
|
- createTime: '2025-01-15'
|
|
|
- },
|
|
|
- {
|
|
|
- id: 5,
|
|
|
- username: 'user004',
|
|
|
- realName: '孙七',
|
|
|
- phone: '13800138005',
|
|
|
- email: 'sunqi@example.com',
|
|
|
- department: '技术部',
|
|
|
- role: '操作员',
|
|
|
- status: '离线',
|
|
|
- lastLogin: '2025-12-03 17:30',
|
|
|
- createTime: '2025-01-20'
|
|
|
- },
|
|
|
- ];
|
|
|
+ // 重置搜索
|
|
|
+ const resetQuery = () => {
|
|
|
+ setQueryParams({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ username: '',
|
|
|
+ mobile: '',
|
|
|
+ status: undefined,
|
|
|
+ workstationId: undefined,
|
|
|
+ createTime: [],
|
|
|
+ });
|
|
|
+ setTimeout(() => getList(), 100);
|
|
|
+ };
|
|
|
|
|
|
- // 用户列表数据和列配置
|
|
|
- const data = userData;
|
|
|
- const columns = [
|
|
|
- { key: 'username', label: '用户名', width: '10%' },
|
|
|
- { key: 'realName', label: '姓名', width: '8%' },
|
|
|
- { key: 'phone', label: '手机号', width: '12%' },
|
|
|
- { key: 'email', label: '邮箱', width: '15%' },
|
|
|
- { key: 'department', label: '部门', width: '10%' },
|
|
|
- { key: 'role', label: '角色', width: '12%' },
|
|
|
- { key: 'status', label: '状态', width: '8%' },
|
|
|
- { key: 'lastLogin', label: '最后登录', width: '15%' },
|
|
|
- ];
|
|
|
+ // 处理部门节点点击
|
|
|
+ const handleDeptNodeClick = (node: WorkstationNode) => {
|
|
|
+ setQueryParams(prev => ({ ...prev, workstationId: node.id, pageNo: 1 }));
|
|
|
+ };
|
|
|
|
|
|
- // 过滤数据
|
|
|
- const filteredData = data.filter((item) =>
|
|
|
- Object.values(item).some((value) =>
|
|
|
- String(value).toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
- )
|
|
|
- );
|
|
|
+ // 打开表单
|
|
|
+ const openForm = (type: string, id?: number) => {
|
|
|
+ formRef.current?.open(type, id);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 用户导入
|
|
|
+ const handleImport = () => {
|
|
|
+ importFormRef.current?.open();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 导出用户
|
|
|
+ const handleExport = async () => {
|
|
|
+ if (!confirm('确定要导出用户数据吗?')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- const handleDelete = (id: number) => {
|
|
|
- if (confirm('确定要删除这条数据吗?')) {
|
|
|
- console.log('删除:', id);
|
|
|
+ setExportLoading(true);
|
|
|
+ try {
|
|
|
+ const blob = await userApi.exportUser(queryParams);
|
|
|
+ const url = window.URL.createObjectURL(blob);
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = url;
|
|
|
+ link.download = '用户数据.xls';
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
+ toast.success('导出成功');
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '导出失败');
|
|
|
+ } finally {
|
|
|
+ setExportLoading(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleEdit = (item: TableRow) => {
|
|
|
- setEditingItem(item);
|
|
|
- setShowAddModal(true);
|
|
|
+ // 修改用户状态
|
|
|
+ const handleStatusChange = async (row: UserVO, newChecked: boolean) => {
|
|
|
+ const newStatus = newChecked ? 0 : 1;
|
|
|
+ const text = newStatus === 0 ? '启用' : '停用';
|
|
|
+
|
|
|
+ // 使用更友好的确认框
|
|
|
+ const confirmed = window.confirm(`确认要${text}"${row.username}"用户吗?`);
|
|
|
+ if (!confirmed) {
|
|
|
+ // 用户取消,不更新状态,需要刷新列表以恢复Switch状态
|
|
|
+ await getList();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await userApi.updateUserStatus(row.id, newStatus);
|
|
|
+ toast.success('状态更新成功');
|
|
|
+ await getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '状态更新失败');
|
|
|
+ // 刷新列表以恢复原状态
|
|
|
+ await getList();
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
- // 递归渲染部门树节点
|
|
|
- const renderDepartmentTree = (departments: any[], level: number = 0) => {
|
|
|
- return departments.map((dept) => (
|
|
|
- <div key={dept.id}>
|
|
|
- {/* 部门节点 */}
|
|
|
- <div
|
|
|
- onClick={() => {
|
|
|
- if (dept.children && dept.children.length > 0) {
|
|
|
- toggleDeptExpand(dept.id);
|
|
|
- }
|
|
|
- setSelectedDeptId(dept.id);
|
|
|
- }}
|
|
|
- className={`flex items-center gap-2 px-3 py-2.5 rounded-lg cursor-pointer transition-all group ${
|
|
|
- selectedDeptId === dept.id
|
|
|
- ? 'bg-blue-500 text-white shadow-md'
|
|
|
- : 'hover:bg-blue-50 text-gray-700'
|
|
|
- }`}
|
|
|
- style={{ paddingLeft: `${level * 20 + 12}px` }}
|
|
|
- >
|
|
|
- {/* 展开/收起图标 */}
|
|
|
- {dept.children && dept.children.length > 0 ? (
|
|
|
- expandedDeptIds.includes(dept.id) ? (
|
|
|
- <ChevronDown className={`w-4 h-4 flex-shrink-0 ${
|
|
|
- selectedDeptId === dept.id ? 'text-white' : 'text-gray-600'
|
|
|
- }`} />
|
|
|
- ) : (
|
|
|
- <ChevronRight className={`w-4 h-4 flex-shrink-0 ${
|
|
|
- selectedDeptId === dept.id ? 'text-white' : 'text-gray-600'
|
|
|
- }`} />
|
|
|
- )
|
|
|
- ) : (
|
|
|
- <span className="w-4"></span>
|
|
|
- )}
|
|
|
-
|
|
|
- {/* 部门图标 */}
|
|
|
- <Building2 className={`w-4 h-4 flex-shrink-0 ${
|
|
|
- selectedDeptId === dept.id ? 'text-white' : 'text-blue-500'
|
|
|
- }`} />
|
|
|
-
|
|
|
- {/* 部门名称和人数 */}
|
|
|
- <div className="flex-1 flex items-center justify-between min-w-0">
|
|
|
- <span className="text-sm truncate">{dept.name}</span>
|
|
|
- <span className={`text-xs px-2 py-0.5 rounded-full flex-shrink-0 ml-2 ${
|
|
|
- selectedDeptId === dept.id
|
|
|
- ? 'bg-white/20 text-white'
|
|
|
- : 'bg-gray-100 text-gray-600 group-hover:bg-blue-100 group-hover:text-blue-700'
|
|
|
- }`}>
|
|
|
- {dept.memberCount}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 子部门 */}
|
|
|
- {dept.children && dept.children.length > 0 && expandedDeptIds.includes(dept.id) && (
|
|
|
- <div className="mt-1">
|
|
|
- {renderDepartmentTree(dept.children, level + 1)}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- ));
|
|
|
+ // 删除用户
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
+ if (!confirm('确定要删除这条数据吗?')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await userApi.deleteUser(id);
|
|
|
+ toast.success('删除成功');
|
|
|
+ await getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '删除失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重置密码
|
|
|
+ const handleResetPwd = async (row: UserVO) => {
|
|
|
+ const password = prompt(`请输入"${row.username}"的新密码`, '');
|
|
|
+ if (!password) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await userApi.resetUserPassword(row.id, password);
|
|
|
+ toast.success(`修改成功,新密码是:${password}`);
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '重置密码失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 分配角色
|
|
|
+ const handleRole = (row: UserVO) => {
|
|
|
+ assignRoleFormRef.current?.open(row);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开指纹或人脸弹框
|
|
|
+ const openFaceOrFingerForm = (type: string, row: UserVO) => {
|
|
|
+ faceOrFingerFormRef.current?.open(type, row.id, row);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 查看岗位
|
|
|
+ const handleLookWorkStation = (row: UserVO) => {
|
|
|
+ // 可以跳转到岗位管理页面,这里暂时用提示
|
|
|
+ toast.info('查看岗位功能,可跳转到岗位管理页面');
|
|
|
+ };
|
|
|
+
|
|
|
+ // 格式化日期
|
|
|
+ const formatDate = (dateStr?: string | Date) => {
|
|
|
+ if (!dateStr) return '';
|
|
|
+ const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
|
|
|
+ return date.toLocaleString('zh-CN');
|
|
|
};
|
|
|
|
|
|
- // 用户列表页面使用左右分栏布局
|
|
|
return (
|
|
|
<div className="flex gap-6 h-full">
|
|
|
- {/* 左侧部门树 */}
|
|
|
+ {/* 左侧岗位树 */}
|
|
|
<div className="w-80 flex-shrink-0">
|
|
|
<div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm h-full overflow-hidden flex flex-col">
|
|
|
- {/* 部门树头部 */}
|
|
|
- <div className="px-4 py-4 border-b border-gray-200/50 flex-shrink-0">
|
|
|
- <div className="flex items-center justify-between mb-3">
|
|
|
- <h3 className="text-sm text-gray-900">组织架构</h3>
|
|
|
- <button className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors">
|
|
|
- <Plus className="w-4 h-4 text-gray-600" />
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div className="relative">
|
|
|
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- placeholder="搜索部门..."
|
|
|
- className="w-full h-9 pl-9 pr-3 bg-gray-50 border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 部门树内容 */}
|
|
|
- <div className="flex-1 overflow-y-auto p-3">
|
|
|
- <div className="space-y-1">
|
|
|
- {renderDepartmentTree(departmentTreeData)}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 底部统计 */}
|
|
|
- <div className="px-4 py-3 bg-gray-50/50 border-t border-gray-200/50 flex-shrink-0">
|
|
|
- <div className="text-xs text-gray-600 text-center">
|
|
|
- 共 <span className="text-blue-600">120</span> 人
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <DeptTree onNodeClick={handleDeptNodeClick} />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* 右侧用户列表 */}
|
|
|
<div className="flex-1 min-w-0">
|
|
|
<div className="space-y-6">
|
|
|
+ {/* 搜索栏 */}
|
|
|
+ <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4">
|
|
|
+ {/* 一行:检索条件和按钮 */}
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <label className="text-sm text-gray-700 whitespace-nowrap">工号:</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={queryParams.username}
|
|
|
+ onChange={(e) => setQueryParams({ ...queryParams, username: e.target.value })}
|
|
|
+ onKeyUp={(e) => e.key === 'Enter' && handleQuery()}
|
|
|
+ placeholder="请输入工号"
|
|
|
+ className="w-48 h-9 px-3 bg-gray-50 border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all text-sm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <label className="text-sm text-gray-700 whitespace-nowrap">手机号码:</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={queryParams.mobile}
|
|
|
+ onChange={(e) => setQueryParams({ ...queryParams, mobile: e.target.value })}
|
|
|
+ onKeyUp={(e) => e.key === 'Enter' && handleQuery()}
|
|
|
+ placeholder="请输入手机号码"
|
|
|
+ className="w-48 h-9 px-3 bg-gray-50 border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all text-sm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Button
|
|
|
+ onClick={handleQuery}
|
|
|
+ className="bg-blue-500 hover:bg-blue-600 text-white border-0"
|
|
|
+ >
|
|
|
+ <Search className="w-4 h-4 mr-2" />
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ onClick={resetQuery}
|
|
|
+ className="bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300"
|
|
|
+ >
|
|
|
+ <RefreshCw className="w-4 h-4 mr-2" />
|
|
|
+ 重置
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={() => openForm('create')}
|
|
|
+ className="bg-green-500 hover:bg-green-600 text-white border-0"
|
|
|
+ >
|
|
|
+ <Plus className="w-4 h-4 mr-2" />
|
|
|
+ 新增
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={handleImport}
|
|
|
+ className="bg-orange-500 hover:bg-orange-600 text-white border-0"
|
|
|
+ >
|
|
|
+ <Upload className="w-4 h-4 mr-2" />
|
|
|
+ 导入
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={handleExport}
|
|
|
+ className="bg-green-500 hover:bg-indigo-700 text-white border-1 disabled:bg-gray-400 disabled:hover:bg-gray-400"
|
|
|
+ >
|
|
|
+ <Download className="w-4 h-4 mr-2" />
|
|
|
+ 导出
|
|
|
+ </Button>
|
|
|
+ {/*<Button*/}
|
|
|
+ {/* onClick={handleExport}*/}
|
|
|
+ {/* disabled={exportLoading}*/}
|
|
|
+ {/* className="bg-indigo-600 hover:bg-indigo-700 text-white border-0 disabled:bg-gray-400 disabled:hover:bg-gray-400"*/}
|
|
|
+ {/*>*/}
|
|
|
+ {/* <Download className="w-4 h-4 mr-2" />*/}
|
|
|
+ {/* {exportLoading ? '导出中...' : '导出'}*/}
|
|
|
+ {/*</Button>*/}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
{/* 表格容器 */}
|
|
|
<div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
|
|
|
- {/* 工具栏 */}
|
|
|
+ {/* 工具栏 - 显示已筛选岗位 */}
|
|
|
+ {queryParams.workstationId && (
|
|
|
<div className="p-4 border-b border-gray-200/50">
|
|
|
- <div className="flex items-center justify-between">
|
|
|
<div className="flex items-center gap-3">
|
|
|
- <div className="relative w-80">
|
|
|
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- placeholder="搜索用户..."
|
|
|
- value={searchTerm}
|
|
|
- onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
- className="w-full h-10 pl-10 pr-4 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- />
|
|
|
- </div>
|
|
|
- {selectedDeptId && (
|
|
|
<div className="flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm">
|
|
|
- <Building2 className="w-4 h-4" />
|
|
|
- <span>已筛选部门</span>
|
|
|
+ <span>已筛选岗位</span>
|
|
|
<button
|
|
|
- onClick={() => setSelectedDeptId(null)}
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, workstationId: undefined, pageNo: 1 })}
|
|
|
className="ml-1 hover:bg-blue-100 rounded p-0.5"
|
|
|
>
|
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
@@ -355,21 +299,9 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
</svg>
|
|
|
</button>
|
|
|
</div>
|
|
|
- )}
|
|
|
</div>
|
|
|
-
|
|
|
- <button
|
|
|
- onClick={() => {
|
|
|
- setEditingItem(null);
|
|
|
- setShowAddModal(true);
|
|
|
- }}
|
|
|
- className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
|
|
|
- >
|
|
|
- <Plus className="w-4 h-4" strokeWidth={2.5} />
|
|
|
- <span className="text-sm">新增用户</span>
|
|
|
- </button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ )}
|
|
|
|
|
|
{/* 表格 */}
|
|
|
<div className="overflow-x-auto">
|
|
|
@@ -379,72 +311,138 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
<th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
|
|
|
序号
|
|
|
</th>
|
|
|
- {columns.map((column) => (
|
|
|
- <th
|
|
|
- key={column.key}
|
|
|
- className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider"
|
|
|
- style={{ width: column.width }}
|
|
|
- >
|
|
|
- {column.label}
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
|
|
|
+ 用户编号
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
+ 工号
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
+ 用户昵称
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '12%' }}>
|
|
|
+ 手机号码
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
+ 岗位
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
|
|
|
+ 指纹
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
|
|
|
+ 人脸
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
|
|
|
+ 状态
|
|
|
+ </th>
|
|
|
+ <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '12%' }}>
|
|
|
+ 创建时间
|
|
|
</th>
|
|
|
- ))}
|
|
|
<th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
|
|
|
操作
|
|
|
</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody className="divide-y divide-gray-100">
|
|
|
- {filteredData.map((row, index) => (
|
|
|
+ {loading ? (
|
|
|
+ <tr>
|
|
|
+ <td colSpan={11} className="px-6 py-8 text-center">
|
|
|
+ <div className="flex items-center justify-center">
|
|
|
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ ) : list.length === 0 ? (
|
|
|
+ <tr>
|
|
|
+ <td colSpan={11} className="px-6 py-8 text-center text-gray-500">
|
|
|
+ 暂无数据
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ ) : (
|
|
|
+ list.map((row, index) => (
|
|
|
<tr
|
|
|
key={row.id}
|
|
|
className="hover:bg-blue-50/30 transition-colors"
|
|
|
>
|
|
|
<td className="px-6 py-4 text-sm text-gray-900">
|
|
|
- {index + 1}
|
|
|
- </td>
|
|
|
- {columns.map((column) => (
|
|
|
- <td key={column.key} className="px-6 py-4 text-sm text-gray-900">
|
|
|
- {column.key === 'status' ? (
|
|
|
- <span
|
|
|
- className={`inline-flex px-3 py-1 rounded-lg text-xs ${
|
|
|
- row[column.key] === '在线'
|
|
|
- ? 'bg-green-100 text-green-700'
|
|
|
- : 'bg-gray-100 text-gray-700'
|
|
|
- }`}
|
|
|
- >
|
|
|
- {row[column.key]}
|
|
|
- </span>
|
|
|
- ) : (
|
|
|
- row[column.key]
|
|
|
- )}
|
|
|
+ {(queryParams.pageNo - 1) * queryParams.pageSize + index + 1}
|
|
|
</td>
|
|
|
- ))}
|
|
|
- <td className="px-6 py-4">
|
|
|
- <div className="flex items-center justify-center gap-2">
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{row.id}</td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{row.username}</td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{row.nickname}</td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">{row.mobile || '-'}</td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
<button
|
|
|
- onClick={() => handleEdit(row)}
|
|
|
- className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
|
|
- title="编辑"
|
|
|
+ onClick={() => handleLookWorkStation(row)}
|
|
|
+ className="text-blue-600 hover:text-blue-800 hover:underline"
|
|
|
>
|
|
|
- <Edit2 className="w-4 h-4" />
|
|
|
+ {row.workstationName || '查看'}
|
|
|
</button>
|
|
|
+ </td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
<button
|
|
|
- onClick={() => handleDelete(row.id)}
|
|
|
- className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
|
|
- title="删除"
|
|
|
+ onClick={() => openFaceOrFingerForm('finger', row)}
|
|
|
+ className="text-blue-600 hover:text-blue-800 hover:underline"
|
|
|
>
|
|
|
- <Trash2 className="w-4 h-4" />
|
|
|
+ 查看
|
|
|
</button>
|
|
|
+ </td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
<button
|
|
|
- className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
- title="更多"
|
|
|
+ onClick={() => openFaceOrFingerForm('face', row)}
|
|
|
+ className="text-blue-600 hover:text-blue-800 hover:underline"
|
|
|
>
|
|
|
- <MoreVertical className="w-4 h-4" />
|
|
|
+ 查看
|
|
|
</button>
|
|
|
+ </td>
|
|
|
+ <td className="px-6 py-4 text-sm">
|
|
|
+ <Switch
|
|
|
+ checked={row.status === 0}
|
|
|
+ onCheckedChange={(checked) => handleStatusChange(row, checked)}
|
|
|
+ className="data-[state=checked]:!bg-blue-500 data-[state=unchecked]:!bg-gray-400"
|
|
|
+ />
|
|
|
+ </td>
|
|
|
+ <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
+ {formatDate(row.createTime)}
|
|
|
+ </td>
|
|
|
+ <td className="px-6 py-4">
|
|
|
+ <div className="flex items-center justify-center gap-2">
|
|
|
+ <button
|
|
|
+ onClick={() => openForm('update', row.id)}
|
|
|
+ className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
|
|
+ title="编辑"
|
|
|
+ >
|
|
|
+ <Edit2 className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ <DropdownMenu>
|
|
|
+ <DropdownMenuTrigger asChild>
|
|
|
+ <button
|
|
|
+ className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors relative z-10"
|
|
|
+ title="更多"
|
|
|
+ >
|
|
|
+ <MoreVertical className="w-4 h-4" />
|
|
|
+ </button>
|
|
|
+ </DropdownMenuTrigger>
|
|
|
+ <DropdownMenuContent align="end" className="!z-[9999]">
|
|
|
+ <DropdownMenuItem onClick={() => handleDelete(row.id)}>
|
|
|
+ <Trash2 className="w-4 h-4 mr-2" />
|
|
|
+ 删除
|
|
|
+ </DropdownMenuItem>
|
|
|
+ <DropdownMenuItem onClick={() => handleResetPwd(row)}>
|
|
|
+ <Key className="w-4 h-4 mr-2" />
|
|
|
+ 重置密码
|
|
|
+ </DropdownMenuItem>
|
|
|
+ <DropdownMenuItem onClick={() => handleRole(row)}>
|
|
|
+ <UserCog className="w-4 h-4 mr-2" />
|
|
|
+ 分配角色
|
|
|
+ </DropdownMenuItem>
|
|
|
+ </DropdownMenuContent>
|
|
|
+ </DropdownMenu>
|
|
|
</div>
|
|
|
</td>
|
|
|
</tr>
|
|
|
- ))}
|
|
|
+ ))
|
|
|
+ )}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
@@ -453,24 +451,28 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
<div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
|
|
|
<div className="flex items-center justify-between">
|
|
|
<div className="text-sm text-gray-600">
|
|
|
- 共 <span className="text-blue-600">{filteredData.length}</span> 条记录
|
|
|
+ 共 <span className="text-blue-600">{total}</span> 条记录
|
|
|
</div>
|
|
|
<div className="flex gap-2">
|
|
|
- <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
|
|
|
+ disabled={queryParams.pageNo <= 1}
|
|
|
+ >
|
|
|
上一页
|
|
|
- </button>
|
|
|
- <button className="px-4 py-2 text-sm text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors">
|
|
|
- 1
|
|
|
- </button>
|
|
|
- <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
- 2
|
|
|
- </button>
|
|
|
- <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
- 3
|
|
|
- </button>
|
|
|
- <button className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
|
|
+ </Button>
|
|
|
+ <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
|
|
|
+ {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize) || 1}
|
|
|
+ </span>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
|
|
|
+ disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
|
|
|
+ >
|
|
|
下一页
|
|
|
- </button>
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -478,128 +480,11 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {/* 新增/编辑用户弹窗 */}
|
|
|
- {showAddModal && (
|
|
|
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
|
|
|
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto animate-in zoom-in duration-200">
|
|
|
- {/* 弹窗标题 */}
|
|
|
- <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
|
|
|
- <h3 className="text-lg text-gray-900">
|
|
|
- {editingItem ? '编辑用户' : '新增用户'}
|
|
|
- </h3>
|
|
|
- <button
|
|
|
- onClick={() => setShowAddModal(false)}
|
|
|
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
|
- >
|
|
|
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 弹窗内容 */}
|
|
|
- <div className="px-6 py-6">
|
|
|
- <div className="grid grid-cols-2 gap-4">
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">用户名 *</label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- placeholder="请输入用户名"
|
|
|
- defaultValue={editingItem?.username || ''}
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">真实姓名 *</label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- placeholder="请输入真实姓名"
|
|
|
- defaultValue={editingItem?.realName || ''}
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">手机号 *</label>
|
|
|
- <input
|
|
|
- type="tel"
|
|
|
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- placeholder="请输入手机号"
|
|
|
- defaultValue={editingItem?.phone || ''}
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">邮箱 *</label>
|
|
|
- <input
|
|
|
- type="email"
|
|
|
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- placeholder="请输入邮箱"
|
|
|
- defaultValue={editingItem?.email || ''}
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">所属部门 *</label>
|
|
|
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
|
|
|
- <option value="">请选择部门</option>
|
|
|
- <option value="技术部">技术部</option>
|
|
|
- <option value="运维部">运维部</option>
|
|
|
- <option value="安全部">安全部</option>
|
|
|
- <option value="测试部">测试部</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">角色 *</label>
|
|
|
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
|
|
|
- <option value="">请选择角色</option>
|
|
|
- <option value="超级管理员">超级管理员</option>
|
|
|
- <option value="部门管理员">部门管理员</option>
|
|
|
- <option value="操作员">操作员</option>
|
|
|
- <option value="审计员">审计员</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- {!editingItem && (
|
|
|
- <>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">密码 *</label>
|
|
|
- <input
|
|
|
- type="password"
|
|
|
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- placeholder="请输入密码"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <label className="block text-sm text-gray-700 mb-2">确认密码 *</label>
|
|
|
- <input
|
|
|
- type="password"
|
|
|
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
|
|
|
- placeholder="请再次输入密码"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 弹窗底部 */}
|
|
|
- <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
|
|
|
- <button
|
|
|
- onClick={() => setShowAddModal(false)}
|
|
|
- className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
|
|
|
- >
|
|
|
- 取消
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={() => {
|
|
|
- console.log('保存:', editingItem);
|
|
|
- setShowAddModal(false);
|
|
|
- }}
|
|
|
- className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
|
|
|
- >
|
|
|
- {editingItem ? '保存修改' : '确定'}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ {/* 子组件 */}
|
|
|
+ <UserForm ref={formRef} onSuccess={getList} />
|
|
|
+ <UserImportForm ref={importFormRef} onSuccess={getList} />
|
|
|
+ <UserAssignRoleForm ref={assignRoleFormRef} onSuccess={getList} />
|
|
|
+ <FaceOrFingerForm ref={faceOrFingerFormRef} onSuccess={getList} />
|
|
|
</div>
|
|
|
);
|
|
|
}
|