312 lines
10 KiB
JavaScript
312 lines
10 KiB
JavaScript
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;
|