|
|
@@ -1,22 +1,18 @@
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
|
-import { Plus, Search, Edit2, Trash2, MoreVertical, Upload, Download, Key, UserCog, Eye, RefreshCw } from 'lucide-react';
|
|
|
+import { Plus, Search, Upload, Download, RefreshCw } from 'lucide-react';
|
|
|
import { userApi } from '../api/user';
|
|
|
import { userImportApi } from '../api/user/import';
|
|
|
+import { postApi, PostVO } from '../api/Post';
|
|
|
import { UserVO, WorkstationNode } from '../types';
|
|
|
import { toast } from 'sonner';
|
|
|
+import { Modal, Table, Input, Button, Switch, Dropdown, Space, Tooltip, Form } from 'antd';
|
|
|
+import { ExclamationCircleOutlined, EditOutlined, DeleteOutlined, KeyOutlined, UserSwitchOutlined, MoreOutlined } from '@ant-design/icons';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
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;
|
|
|
@@ -26,6 +22,7 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [list, setList] = useState<UserVO[]>([]);
|
|
|
const [total, setTotal] = useState(0);
|
|
|
+ const [postList, setPostList] = useState<PostVO[]>([]);
|
|
|
const [queryParams, setQueryParams] = useState({
|
|
|
pageNo: 1,
|
|
|
pageSize: 10,
|
|
|
@@ -36,6 +33,10 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
createTime: [] as string[],
|
|
|
});
|
|
|
const [exportLoading, setExportLoading] = useState(false);
|
|
|
+ const [resetPwdModalVisible, setResetPwdModalVisible] = useState(false);
|
|
|
+ const [resetPwdUser, setResetPwdUser] = useState<UserVO | null>(null);
|
|
|
+ const [resetPwdPassword, setResetPwdPassword] = useState('');
|
|
|
+ const [resetPwdLoading, setResetPwdLoading] = useState(false);
|
|
|
|
|
|
// 子组件引用
|
|
|
const formRef = useRef<UserFormRef>(null);
|
|
|
@@ -58,6 +59,22 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ // 加载岗位列表
|
|
|
+ useEffect(() => {
|
|
|
+ const loadPostList = async () => {
|
|
|
+ try {
|
|
|
+ const response = await postApi.getSimplePostList();
|
|
|
+ // 处理响应数据,可能是直接返回数组,也可能包装在 data 中
|
|
|
+ const posts = (response as any)?.data || response;
|
|
|
+ setPostList(Array.isArray(posts) ? posts : []);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载岗位列表失败:', error);
|
|
|
+ setPostList([]);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ loadPostList();
|
|
|
+ }, []);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
getList();
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
@@ -107,80 +124,121 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
|
|
|
// 导出用户
|
|
|
const handleExport = async () => {
|
|
|
- if (!confirm('确定要导出用户数据吗?')) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- setExportLoading(true);
|
|
|
- try {
|
|
|
- const blob = await userImportApi.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);
|
|
|
- }
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认导出',
|
|
|
+ icon: <ExclamationCircleOutlined />,
|
|
|
+ content: '确定要导出用户数据吗?',
|
|
|
+ okText: '确定导出',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async () => {
|
|
|
+ setExportLoading(true);
|
|
|
+ try {
|
|
|
+ const blob = await userImportApi.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 handleStatusChange = async (row: UserVO, newChecked: boolean) => {
|
|
|
- const newStatus = newChecked ? 0 : 1;
|
|
|
+ const newStatus = newChecked ? 0 : 1; // 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();
|
|
|
- }
|
|
|
+ // 使用 antd 的确认弹框
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认操作',
|
|
|
+ icon: <ExclamationCircleOutlined />,
|
|
|
+ content: `确定要${text}用户"${row.username}"吗?`,
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async () => {
|
|
|
+ try {
|
|
|
+ await userApi.updateUserStatus(row.id, newStatus);
|
|
|
+ toast.success(`用户${text}成功`);
|
|
|
+ // 刷新列表以更新状态
|
|
|
+ await getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || `用户${text}失败`);
|
|
|
+ // 接口调用失败,刷新列表以恢复Switch状态
|
|
|
+ await getList();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onCancel: async () => {
|
|
|
+ // 用户取消操作,立即刷新列表以恢复Switch状态
|
|
|
+ // 由于Switch是受控组件,刷新列表后会自动恢复到原始状态(基于row.status)
|
|
|
+ await getList();
|
|
|
+ },
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
// 删除用户
|
|
|
- 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 handleDelete = async (id: number, username?: string) => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认删除',
|
|
|
+ icon: <ExclamationCircleOutlined />,
|
|
|
+ content: (
|
|
|
+ <div>
|
|
|
+ <p>确定要删除用户 <strong>"{username || '该用户'}"</strong> 吗?</p>
|
|
|
+ <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ okText: '确定删除',
|
|
|
+ okType: 'danger',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async () => {
|
|
|
+ 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) {
|
|
|
+ const handleResetPwd = (row: UserVO) => {
|
|
|
+ setResetPwdUser(row);
|
|
|
+ setResetPwdPassword('');
|
|
|
+ setResetPwdModalVisible(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 提交重置密码
|
|
|
+ const submitResetPassword = async () => {
|
|
|
+ if (!resetPwdPassword.trim()) {
|
|
|
+ toast.error('请输入新密码');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ if (!resetPwdUser) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setResetPwdLoading(true);
|
|
|
try {
|
|
|
- await userApi.resetUserPassword(row.id, password);
|
|
|
- toast.success(`修改成功,新密码是:${password}`);
|
|
|
+ await userApi.resetUserPassword(resetPwdUser.id, resetPwdPassword);
|
|
|
+ toast.success(`修改成功,新密码是:${resetPwdPassword}`);
|
|
|
+ setResetPwdModalVisible(false);
|
|
|
+ setResetPwdPassword('');
|
|
|
+ setResetPwdUser(null);
|
|
|
} catch (error: any) {
|
|
|
toast.error(error.message || '重置密码失败');
|
|
|
+ } finally {
|
|
|
+ setResetPwdLoading(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -194,18 +252,158 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
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');
|
|
|
- };
|
|
|
+ // 表格列配置
|
|
|
+ const columns: ColumnsType<UserVO> = [
|
|
|
+ {
|
|
|
+ title: '序号',
|
|
|
+ width: '5%',
|
|
|
+ render: (_: any, __: UserVO, index: number) => {
|
|
|
+ return (queryParams.pageNo - 1) * queryParams.pageSize + index + 1;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '用户编号',
|
|
|
+ dataIndex: 'id',
|
|
|
+ width: '8%',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '账号',
|
|
|
+ dataIndex: 'username',
|
|
|
+ width: '10%',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '用户昵称',
|
|
|
+ dataIndex: 'nickname',
|
|
|
+ width: '10%',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '手机号码',
|
|
|
+ dataIndex: 'mobile',
|
|
|
+ width: '12%',
|
|
|
+ render: (text: string) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '部门',
|
|
|
+ width: '10%',
|
|
|
+ render: (_: any, record: UserVO) => {
|
|
|
+ return (record as any).deptName || '-';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '岗位',
|
|
|
+ width: '10%',
|
|
|
+ render: (_: any, record: UserVO) => {
|
|
|
+ // 根据 postIds 匹配岗位名称
|
|
|
+ if (record.postIds && Array.isArray(record.postIds) && record.postIds.length > 0) {
|
|
|
+ const postNames = record.postIds
|
|
|
+ .map((postId: string | number) => {
|
|
|
+ // 将 postId 转换为数字进行比较
|
|
|
+ const id = typeof postId === 'string' ? Number(postId) : postId;
|
|
|
+ const post = postList.find(p => p.id === id);
|
|
|
+ return post ? post.name : null;
|
|
|
+ })
|
|
|
+ .filter((name: string | null) => name !== null);
|
|
|
+
|
|
|
+ const displayText = postNames.length > 0 ? postNames.join(',') : '-';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Tooltip
|
|
|
+ title={
|
|
|
+ <div style={{ maxHeight: '200px', overflowY: 'auto', maxWidth: '300px' }}>
|
|
|
+ {displayText}
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ placement="topLeft"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ overflow: 'hidden',
|
|
|
+ textOverflow: 'ellipsis',
|
|
|
+ whiteSpace: 'nowrap',
|
|
|
+ cursor: 'help',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {displayText}
|
|
|
+ </div>
|
|
|
+ </Tooltip>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return '-';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '人脸',
|
|
|
+ width: '8%',
|
|
|
+ render: (_: any, record: UserVO) => (
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ onClick={() => openFaceOrFingerForm('face', record)}
|
|
|
+ style={{ padding: 0 }}
|
|
|
+ >
|
|
|
+ 查看
|
|
|
+ </Button>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ width: '8%',
|
|
|
+ render: (_: any, record: UserVO) => (
|
|
|
+ <Switch
|
|
|
+ checked={record.status === 0}
|
|
|
+ onChange={(checked) => handleStatusChange(record, checked)}
|
|
|
+ />
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ width: '10%',
|
|
|
+ align: 'center',
|
|
|
+ render: (_: any, record: UserVO) => {
|
|
|
+ const menuItems = [
|
|
|
+ {
|
|
|
+ key: 'delete',
|
|
|
+ label: '删除',
|
|
|
+ icon: <DeleteOutlined />,
|
|
|
+ onClick: () => handleDelete(record.id, record.username),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'resetPwd',
|
|
|
+ label: '重置密码',
|
|
|
+ icon: <KeyOutlined />,
|
|
|
+ onClick: () => handleResetPwd(record),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'assignRole',
|
|
|
+ label: '分配角色',
|
|
|
+ icon: <UserSwitchOutlined />,
|
|
|
+ onClick: () => handleRole(record),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ onClick={() => openForm('update', record.id)}
|
|
|
+ title="编辑"
|
|
|
+ />
|
|
|
+ <Dropdown
|
|
|
+ menu={{ items: menuItems }}
|
|
|
+ trigger={['click']}
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ icon={<MoreOutlined />}
|
|
|
+ title="更多"
|
|
|
+ />
|
|
|
+ </Dropdown>
|
|
|
+ </Space>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ];
|
|
|
|
|
|
return (
|
|
|
<div className="flex gap-6 h-full">
|
|
|
@@ -220,253 +418,98 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
<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 className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
|
|
|
+ <div className="flex items-center justify-between gap-4 flex-wrap">
|
|
|
+ {/* 搜索输入框 */}
|
|
|
+ <div className="flex items-center gap-3 flex-wrap flex-1">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <label className="text-sm font-medium text-gray-700 whitespace-nowrap">账号:</label>
|
|
|
+ <Input
|
|
|
+ value={queryParams.username}
|
|
|
+ onChange={(e) => setQueryParams({ ...queryParams, username: e.target.value })}
|
|
|
+ onPressEnter={handleQuery}
|
|
|
+ placeholder="请输入账号"
|
|
|
+ style={{ width: 192 }}
|
|
|
+ allowClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <label className="text-sm font-medium text-gray-700 whitespace-nowrap">手机号码:</label>
|
|
|
+ <Input
|
|
|
+ value={queryParams.mobile}
|
|
|
+ onChange={(e) => setQueryParams({ ...queryParams, mobile: e.target.value })}
|
|
|
+ onPressEnter={handleQuery}
|
|
|
+ placeholder="请输入手机号码"
|
|
|
+ style={{ width: 192 }}
|
|
|
+ allowClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</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
|
|
|
+
|
|
|
+ {/* 操作按钮组 */}
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<Search className="w-4 h-4" />}
|
|
|
+ onClick={handleQuery}
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ icon={<RefreshCw className="w-4 h-4" />}
|
|
|
+ onClick={resetQuery}
|
|
|
+ >
|
|
|
+ 重置
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<Plus className="w-4 h-4" />}
|
|
|
+ onClick={() => openForm('create')}
|
|
|
+ >
|
|
|
+ 新增
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ icon={<Upload className="w-4 h-4" />}
|
|
|
+ onClick={handleImport}
|
|
|
+ >
|
|
|
+ 导入
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ icon={<Download className="w-4 h-4" />}
|
|
|
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>*/}
|
|
|
+ loading={exportLoading}
|
|
|
+ >
|
|
|
+ 导出
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
</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 gap-3">
|
|
|
- <div className="flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm">
|
|
|
- <span>已筛选岗位</span>
|
|
|
- <button
|
|
|
- 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">
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {/* 表格 */}
|
|
|
- <div className="overflow-x-auto">
|
|
|
- <table className="w-full">
|
|
|
- <thead>
|
|
|
- <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
|
|
|
- <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
|
|
|
- 序号
|
|
|
- </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: '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">
|
|
|
- {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">
|
|
|
- {(queryParams.pageNo - 1) * queryParams.pageSize + index + 1}
|
|
|
- </td>
|
|
|
- <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={() => handleLookWorkStation(row)}
|
|
|
- className="text-blue-600 hover:text-blue-800 hover:underline"
|
|
|
- >
|
|
|
- {row.workstationName || '查看'}
|
|
|
- </button>
|
|
|
- </td>
|
|
|
- <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
- <button
|
|
|
- onClick={() => openFaceOrFingerForm('finger', row)}
|
|
|
- className="text-blue-600 hover:text-blue-800 hover:underline"
|
|
|
- >
|
|
|
- 查看
|
|
|
- </button>
|
|
|
- </td>
|
|
|
- <td className="px-6 py-4 text-sm text-gray-900">
|
|
|
- <button
|
|
|
- onClick={() => openFaceOrFingerForm('face', row)}
|
|
|
- className="text-blue-600 hover:text-blue-800 hover:underline"
|
|
|
- >
|
|
|
- 查看
|
|
|
- </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>
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={list}
|
|
|
+ rowKey="id"
|
|
|
+ loading={loading}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ x: 'max-content' }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 分页 */}
|
|
|
- <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
|
|
|
+ {/* 分页 */}
|
|
|
+ {!loading && list.length > 0 && (
|
|
|
+ <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
|
|
|
<div className="flex items-center justify-between">
|
|
|
<div className="text-sm text-gray-600">
|
|
|
- 共 <span className="text-blue-600">{total}</span> 条记录
|
|
|
+ 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
|
|
|
</div>
|
|
|
<div className="flex gap-2">
|
|
|
<Button
|
|
|
- variant="outline"
|
|
|
- size="sm"
|
|
|
onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
|
|
|
disabled={queryParams.pageNo <= 1}
|
|
|
>
|
|
|
@@ -476,8 +519,6 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
{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)}
|
|
|
>
|
|
|
@@ -486,7 +527,7 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -495,6 +536,35 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
|
|
|
<UserImportForm ref={importFormRef} onSuccess={getList} />
|
|
|
<UserAssignRoleForm ref={assignRoleFormRef} onSuccess={getList} />
|
|
|
<FaceOrFingerForm ref={faceOrFingerFormRef} onSuccess={getList} />
|
|
|
+
|
|
|
+ {/* 重置密码弹框 */}
|
|
|
+ <Modal
|
|
|
+ title="重置密码"
|
|
|
+ open={resetPwdModalVisible}
|
|
|
+ onOk={submitResetPassword}
|
|
|
+ onCancel={() => {
|
|
|
+ setResetPwdModalVisible(false);
|
|
|
+ setResetPwdPassword('');
|
|
|
+ setResetPwdUser(null);
|
|
|
+ }}
|
|
|
+ confirmLoading={resetPwdLoading}
|
|
|
+ okText="确定"
|
|
|
+ cancelText="取消"
|
|
|
+ width={500}
|
|
|
+ >
|
|
|
+ <div style={{ marginTop: 16 }}>
|
|
|
+ <p style={{ marginBottom: 12 }}>
|
|
|
+ 请输入用户 <strong>"{resetPwdUser?.username}"</strong> 的新密码:
|
|
|
+ </p>
|
|
|
+ <Input.Password
|
|
|
+ placeholder="请输入新密码"
|
|
|
+ value={resetPwdPassword}
|
|
|
+ onChange={(e) => setResetPwdPassword(e.target.value)}
|
|
|
+ onPressEnter={submitResetPassword}
|
|
|
+ autoFocus
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
</div>
|
|
|
);
|
|
|
}
|