imetting/frontend/src/pages/admin/UserManagement.jsx

308 lines
9.9 KiB
React
Raw Normal View History

2026-03-26 06:55:12 +00:00
import React, { useEffect, useMemo, useState } from 'react';
import { Table, Button, Input, Space, Modal, Form, Select, App, Tooltip, Tag } from 'antd';
import {
PlusOutlined,
CloseOutlined,
EditOutlined,
DeleteOutlined,
KeyOutlined,
SaveOutlined,
SearchOutlined,
UserOutlined,
MailOutlined,
SafetyCertificateOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
2026-03-26 06:55:12 +00:00
import configService from '../../utils/configService';
import AdminModuleShell from '../../components/AdminModuleShell';
const UserManagement = () => {
2026-03-26 06:55:12 +00:00
const { message, modal } = App.useApp();
const [form] = Form.useForm();
const [users, setUsers] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [loading, setLoading] = useState(true);
const [showUserModal, setShowUserModal] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [roles, setRoles] = useState([]);
2026-03-26 06:55:12 +00:00
const [searchText, setSearchText] = useState('');
const [debouncedSearchText, setDebouncedSearchText] = useState('');
2026-03-26 06:55:12 +00:00
useEffect(() => {
configService.getPageSize().then((size) => {
const safeSize = Number.isFinite(size) ? Math.min(100, Math.max(5, size)) : 10;
setPageSize(safeSize);
});
fetchRoles();
}, []);
2026-03-26 06:55:12 +00:00
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchText(searchText.trim());
setPage(1);
}, 300);
return () => clearTimeout(timer);
}, [searchText]);
useEffect(() => {
fetchUsers();
2026-03-26 06:55:12 +00:00
}, [page, pageSize, debouncedSearchText]);
const fetchUsers = async () => {
setLoading(true);
try {
let url = `${API_ENDPOINTS.USERS.LIST}?page=${page}&size=${pageSize}`;
2026-03-26 06:55:12 +00:00
if (debouncedSearchText) {
url += `&search=${encodeURIComponent(debouncedSearchText)}`;
}
const response = await apiClient.get(buildApiUrl(url));
2026-03-26 06:55:12 +00:00
setUsers(response.data.users || []);
setTotal(response.data.total || 0);
} catch {
message.error('无法加载用户列表');
} finally {
setLoading(false);
}
};
const fetchRoles = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.ROLES));
2026-03-26 06:55:12 +00:00
setRoles(response.data || []);
} catch {
setRoles([
{ role_id: 1, role_name: '平台管理员' },
2026-03-26 06:55:12 +00:00
{ role_id: 2, role_name: '普通用户' },
]);
}
};
const handleOpenModal = (user = null) => {
if (user) {
setIsEditing(true);
2026-03-26 06:55:12 +00:00
setCurrentUser(user);
form.setFieldsValue(user);
} else {
setIsEditing(false);
2026-03-26 06:55:12 +00:00
setCurrentUser(null);
form.resetFields();
form.setFieldsValue({ role_id: 2 });
}
setShowUserModal(true);
};
const handleSave = async () => {
2026-03-26 06:55:12 +00:00
try {
const values = await form.validateFields();
if (isEditing) {
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(currentUser.user_id)), values);
message.success('用户修改成功');
} else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), values);
message.success('用户添加成功');
}
setShowUserModal(false);
fetchUsers();
} catch (err) {
if (err.response) {
message.error(err.response.data?.message || '操作失败');
}
}
};
2026-03-26 06:55:12 +00:00
const handleDelete = (user) => {
modal.confirm({
title: '删除用户',
content: `确定要删除用户"${user.caption}"吗?此操作无法撤销。`,
okText: '确定删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(user.user_id)));
message.success('用户删除成功');
fetchUsers();
} catch {
message.error('删除失败');
}
},
});
};
2026-03-26 06:55:12 +00:00
const handleResetPassword = (user) => {
modal.confirm({
title: '重置密码',
content: `确定要重置用户"${user.caption}"的密码吗?重置后密码将恢复为系统默认密码。`,
okText: '确定重置',
cancelText: '取消',
onOk: async () => {
try {
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.RESET_PASSWORD(user.user_id)));
message.success('密码重置成功');
} catch {
message.error('重置失败');
}
},
});
};
2026-03-26 06:55:12 +00:00
const roleNameById = useMemo(() => {
return roles.reduce((map, role) => {
map[role.role_id] = role.role_name;
return map;
}, {});
}, [roles]);
const adminCountOnPage = useMemo(
() => users.filter((user) => String(user.role_id) === '1' || user.role_name?.includes('管理员')).length,
[users],
);
const columns = [
2026-03-26 06:55:12 +00:00
{ title: 'ID', dataIndex: 'user_id', key: 'user_id', width: 80 },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '姓名', dataIndex: 'caption', key: 'caption' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
2026-03-26 06:55:12 +00:00
{
title: '角色',
dataIndex: 'role_name',
key: 'role_name',
render: (text, record) => {
const roleName = text || roleNameById[record.role_id] || '-';
return <Tag color={String(record.role_id) === '1' ? 'blue' : 'default'}>{roleName}</Tag>;
},
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
2026-03-26 06:55:12 +00:00
render: (text) => (text ? new Date(text).toLocaleString() : '-'),
},
{
title: '操作',
key: 'action',
fixed: 'right',
2026-03-26 06:55:12 +00:00
width: 150,
render: (_, record) => (
<Space size="middle">
<Tooltip title="修改">
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
</Tooltip>
<Tooltip title="重置密码">
<Button type="text" className="btn-text-accent" icon={<KeyOutlined />} onClick={() => handleResetPassword(record)} />
</Tooltip>
<Tooltip title="删除">
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
</Tooltip>
</Space>
),
},
];
return (
<div className="user-management">
2026-03-26 06:55:12 +00:00
<AdminModuleShell
icon={<SafetyCertificateOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />}
title="用户管理"
subtitle="管理账号信息、角色分配与密码恢复。支持快速搜索和分页配置。"
rightActions={
<Space>
<Button icon={<ReloadOutlined />} className="btn-soft-blue" onClick={fetchUsers} loading={loading}>刷新</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
新增用户
</Button>
</Space>
}
stats={[
{ label: '总用户数', value: total },
{ label: '当前页', value: `${users.length}/${pageSize}` },
{ label: '管理员(页内)', value: adminCountOnPage },
]}
toolbar={
<Input
placeholder="搜索用户名、姓名或邮箱..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ maxWidth: 320 }}
allowClear
/>
}
>
<div className="console-table">
<Table
columns={columns}
dataSource={users}
loading={loading}
pagination={{
current: page,
pageSize,
total,
onChange: (nextPage, size) => {
setPage(nextPage);
setPageSize(size);
},
showSizeChanger: true,
showTotal: (count) => `${count} 条记录`,
}}
rowKey="user_id"
scroll={{ x: 900 }}
/>
</div>
2026-03-26 06:55:12 +00:00
</AdminModuleShell>
2026-03-26 06:55:12 +00:00
<Modal
open={showUserModal}
title={isEditing ? '编辑用户' : '新增用户'}
2026-03-26 06:55:12 +00:00
onCancel={() => setShowUserModal(false)}
onOk={handleSave}
okText={isEditing ? '保存修改' : '创建用户'}
okButtonProps={{ icon: <SaveOutlined /> }}
cancelButtonProps={{ icon: <CloseOutlined /> }}
destroyOnHidden
>
2026-03-26 06:55:12 +00:00
<Form form={form} layout="vertical" style={{ marginTop: 20 }} initialValues={{ role_id: 2 }}>
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input prefix={<UserOutlined />} placeholder="请输入用户名" />
</Form.Item>
<Form.Item name="caption" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
<Input prefix={<UserOutlined />} placeholder="请输入姓名" />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input prefix={<MailOutlined />} placeholder="请输入邮箱" />
</Form.Item>
<Form.Item name="role_id" label="角色" rules={[{ required: true, message: '请选择角色' }]}>
<Select
options={roles.map((role) => ({
label: role.role_name,
value: role.role_id,
}))}
/>
</Form.Item>
{!isEditing && (
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 12 }}>
新用户默认使用系统初始化密码首次登录后建议立即修改
</div>
2026-03-26 06:55:12 +00:00
)}
</Form>
</Modal>
</div>
);
};
2026-03-26 06:55:12 +00:00
export default UserManagement;