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

312 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useEffect, useMemo, useState } from 'react';
import { Table, Button, Input, Space, Drawer, Form, Select, App, Tooltip, Tag } from 'antd';
import {
PlusOutlined,
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';
import configService from '../../utils/configService';
import AdminModuleShell from '../../components/AdminModuleShell';
const UserManagement = () => {
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 [showUserDrawer, setShowUserDrawer] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [roles, setRoles] = useState([]);
const [searchText, setSearchText] = useState('');
const [debouncedSearchText, setDebouncedSearchText] = useState('');
useEffect(() => {
configService.getPageSize().then((size) => {
const safeSize = Number.isFinite(size) ? Math.min(100, Math.max(5, size)) : 10;
setPageSize(safeSize);
});
fetchRoles();
}, []);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchText(searchText.trim());
setPage(1);
}, 300);
return () => clearTimeout(timer);
}, [searchText]);
useEffect(() => {
fetchUsers();
}, [page, pageSize, debouncedSearchText]);
const fetchUsers = async () => {
setLoading(true);
try {
let url = `${API_ENDPOINTS.USERS.LIST}?page=${page}&size=${pageSize}`;
if (debouncedSearchText) {
url += `&search=${encodeURIComponent(debouncedSearchText)}`;
}
const response = await apiClient.get(buildApiUrl(url));
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));
setRoles(response.data || []);
} catch {
setRoles([
{ role_id: 1, role_name: '平台管理员' },
{ role_id: 2, role_name: '普通用户' },
]);
}
};
const handleOpenModal = (user = null) => {
if (user) {
setIsEditing(true);
setCurrentUser(user);
form.setFieldsValue(user);
} else {
setIsEditing(false);
setCurrentUser(null);
form.resetFields();
form.setFieldsValue({ role_id: 2 });
}
setShowUserDrawer(true);
};
const handleSave = async () => {
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('用户添加成功');
}
setShowUserDrawer(false);
fetchUsers();
} catch (err) {
if (err.response) {
message.error(err.response.data?.message || '操作失败');
}
}
};
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('删除失败');
}
},
});
};
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('重置失败');
}
},
});
};
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 = [
{ 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' },
{
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',
render: (text) => (text ? new Date(text).toLocaleString() : '-'),
},
{
title: '操作',
key: 'action',
fixed: 'right',
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">
<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>
</AdminModuleShell>
<Drawer
open={showUserDrawer}
title={isEditing ? '编辑用户' : '新增用户'}
placement="right"
width={520}
onClose={() => setShowUserDrawer(false)}
destroyOnClose
extra={(
<Space>
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
{isEditing ? '保存修改' : '创建用户'}
</Button>
</Space>
)}
>
<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>
)}
</Form>
</Drawer>
</div>
);
};
export default UserManagement;