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';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
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';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
const UserManagement = () => {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const { message, modal } = App.useApp();
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
|
2026-01-19 11:03:08 +00:00
|
|
|
|
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-01-19 11:03:08 +00:00
|
|
|
|
|
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-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
|
setDebouncedSearchText(searchText.trim());
|
|
|
|
|
|
setPage(1);
|
|
|
|
|
|
}, 300);
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
|
}, [searchText]);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchUsers();
|
2026-03-26 06:55:12 +00:00
|
|
|
|
}, [page, pageSize, debouncedSearchText]);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
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)}`;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
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('无法加载用户列表');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} 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 {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
setRoles([
|
|
|
|
|
|
{ role_id: 1, role_name: '平台管理员' },
|
2026-03-26 06:55:12 +00:00
|
|
|
|
{ role_id: 2, role_name: '普通用户' },
|
2026-01-19 11:03:08 +00:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenModal = (user = null) => {
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
setIsEditing(true);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setCurrentUser(user);
|
|
|
|
|
|
form.setFieldsValue(user);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
setIsEditing(false);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setCurrentUser(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({ role_id: 2 });
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
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-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
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-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
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],
|
|
|
|
|
|
);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-02-12 07:34:12 +00:00
|
|
|
|
const columns = [
|
2026-03-26 06:55:12 +00:00
|
|
|
|
{ title: 'ID', dataIndex: 'user_id', key: 'user_id', width: 80 },
|
2026-02-12 07:34:12 +00:00
|
|
|
|
{ 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',
|
2026-02-12 07:34:12 +00:00
|
|
|
|
key: 'created_at',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
render: (text) => (text ? new Date(text).toLocaleString() : '-'),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-02-12 07:34:12 +00:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-19 11:03:08 +00:00
|
|
|
|
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 }}
|
|
|
|
|
|
/>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</AdminModuleShell>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Modal
|
|
|
|
|
|
open={showUserModal}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
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-01-19 11:03:08 +00:00
|
|
|
|
>
|
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 }}>
|
|
|
|
|
|
注:新用户默认使用系统初始化密码,首次登录后建议立即修改。
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
export default UserManagement;
|