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

368 lines
12 KiB
React
Raw Normal View History

import React, { useState, useEffect } from 'react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
2026-01-21 07:21:17 +00:00
import { Plus, Edit, Trash2, KeyRound, User, Mail, Shield, Search, X } from 'lucide-react';
import ConfirmDialog from '../../components/ConfirmDialog';
import FormModal from '../../components/FormModal';
import Toast from '../../components/Toast';
import ListTable from '../../components/ListTable';
import './UserManagement.css';
const UserManagement = () => {
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 [error, setError] = useState('');
const [showUserModal, setShowUserModal] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [resetConfirmInfo, setResetConfirmInfo] = useState(null);
const [roles, setRoles] = useState([]);
const [toasts, setToasts] = useState([]);
const [searchText, setSearchText] = useState(''); // 搜索文本
// Toast helper functions
const showToast = (message, type = 'info') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
};
const removeToast = (id) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
useEffect(() => {
fetchUsers();
fetchRoles();
}, [page, pageSize, searchText]);
const fetchUsers = async () => {
setLoading(true);
try {
let url = `${API_ENDPOINTS.USERS.LIST}?page=${page}&size=${pageSize}`;
if (searchText) {
url += `&search=${encodeURIComponent(searchText)}`;
}
const response = await apiClient.get(buildApiUrl(url));
setUsers(response.data.users);
setTotal(response.data.total);
} catch (err) {
setError('无法加载用户列表');
} finally {
setLoading(false);
}
};
const fetchRoles = async () => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.ROLES));
setRoles(response.data);
} catch (err) {
console.error('Error fetching roles:', err);
// 如果无法获取角色列表,使用默认值
setRoles([
{ role_id: 1, role_name: '平台管理员' },
{ role_id: 2, role_name: '普通用户' }
]);
}
};
const handleAddUser = async (e) => {
e.preventDefault();
try {
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), currentUser);
handleCloseModal();
setError('');
fetchUsers();
showToast('用户添加成功', 'success');
} catch (err) {
console.error('Error adding user:', err);
setError(err.response?.data?.message || '新增用户失败');
}
};
const handleUpdateUser = async (e) => {
e.preventDefault();
try {
const updateData = {
caption: currentUser.caption,
email: currentUser.email,
role_id: currentUser.role_id
};
if (currentUser.username && currentUser.username.trim()) {
updateData.username = currentUser.username;
}
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(currentUser.user_id)), updateData);
handleCloseModal();
setError('');
fetchUsers();
showToast('用户修改成功', 'success');
} catch (err) {
console.error('Error updating user:', err);
setError(err.response?.data?.message || '修改用户失败');
}
};
const handleDeleteUser = async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(deleteConfirmInfo.user_id)));
setDeleteConfirmInfo(null);
showToast('用户删除成功', 'success');
fetchUsers();
} catch (err) {
console.error('Error deleting user:', err);
showToast('删除用户失败,请重试', 'error');
setDeleteConfirmInfo(null);
}
};
const handleResetPassword = async () => {
try {
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.RESET_PASSWORD(resetConfirmInfo.user_id)));
setResetConfirmInfo(null);
showToast('密码重置成功', 'success');
} catch (err) {
console.error('Error resetting password:', err);
showToast('重置密码失败,请重试', 'error');
setResetConfirmInfo(null);
}
};
const handleOpenModal = (user = null) => {
if (user) {
setIsEditing(true);
setCurrentUser({ ...user });
} else {
setIsEditing(false);
setCurrentUser({ username: '', caption: '', email: '', role_id: 2 });
}
setError('');
setShowUserModal(true);
};
const handleCloseModal = () => {
setShowUserModal(false);
setCurrentUser(null);
setError('');
};
const handleSave = async () => {
if (isEditing) {
await handleUpdateUser({ preventDefault: () => {} });
} else {
await handleAddUser({ preventDefault: () => {} });
}
};
const handleInputChange = (field, value) => {
setCurrentUser(prev => ({ ...prev, [field]: value }));
};
const openDeleteConfirm = (user) => {
setDeleteConfirmInfo({ user_id: user.user_id, caption: user.caption });
};
const openResetConfirm = (user) => {
setResetConfirmInfo({ user_id: user.user_id, caption: user.caption });
};
const columns = [
{ title: 'ID', dataIndex: 'user_id', key: 'user_id' },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '姓名', dataIndex: 'caption', key: 'caption' },
{ title: '邮箱', dataIndex: 'email', key: 'email' },
{ title: '角色', dataIndex: 'role_name', key: 'role_name' },
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
render: (item) => new Date(item.created_at).toLocaleString()
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: '150px',
render: (item) => (
<div className="action-buttons" style={{ display: 'flex', gap: '8px', flexWrap: 'nowrap' }}>
<button
onClick={() => handleOpenModal(item)}
title="修改"
style={{ width: '32px', height: '32px', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #e2e8f0', borderRadius: '6px', cursor: 'pointer', background: 'white', color: '#3b82f6' }}
>
<Edit size={16} />
</button>
<button
onClick={() => openDeleteConfirm(item)}
title="删除"
style={{ width: '32px', height: '32px', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #e2e8f0', borderRadius: '6px', cursor: 'pointer', background: 'white', color: '#ef4444' }}
>
<Trash2 size={16} />
</button>
<button
onClick={() => openResetConfirm(item)}
title="重置密码"
style={{ width: '32px', height: '32px', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #e2e8f0', borderRadius: '6px', cursor: 'pointer', background: 'white', color: '#f59e0b' }}
>
<KeyRound size={16} />
</button>
</div>
)
}
];
return (
<div className="user-management">
<div className="toolbar">
<h2>用户列表</h2>
<div className="toolbar-actions">
2026-01-21 07:21:17 +00:00
<div className="search-box">
<Search size={18} />
<input
type="text"
placeholder="搜索用户名或姓名..."
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
setPage(1); // 重置到第一页
}}
/>
{searchText && (
<button className="clear-search" onClick={() => setSearchText('')}>
<X size={16} />
</button>
)}
</div>
<button className="btn btn-primary" onClick={() => handleOpenModal()}><Plus size={16} /> 新增用户</button>
</div>
</div>
{error && <p className="error-message">{error}</p>}
<ListTable
columns={columns}
data={users}
loading={loading}
pagination={{
current: page,
pageSize: pageSize,
total: total,
onChange: (p) => setPage(p)
}}
rowKey="user_id"
showPagination={true}
/>
{/* 用户表单模态框 */}
<FormModal
isOpen={showUserModal}
onClose={handleCloseModal}
title={isEditing ? '编辑用户' : '新增用户'}
size="medium"
actions={
<>
<button type="button" className="btn btn-secondary" onClick={handleCloseModal}>
取消
</button>
<button type="button" className="btn btn-primary" onClick={handleSave}>
{isEditing ? '确认修改' : '确认新增'}
</button>
</>
}
>
{error && <div className="error-message">{error}</div>}
{currentUser && (
<>
<div className="form-group">
<label><User size={16} /> 用户名</label>
<input
type="text"
value={currentUser.username}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder="请输入用户名"
required
/>
</div>
<div className="form-group">
<label><User size={16} /> 姓名</label>
<input
type="text"
value={currentUser.caption}
onChange={(e) => handleInputChange('caption', e.target.value)}
placeholder="请输入姓名"
required
/>
</div>
<div className="form-group">
<label><Mail size={16} /> 邮箱</label>
<input
type="email"
value={currentUser.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="请输入邮箱"
required
/>
</div>
<div className="form-group">
<label><Shield size={16} /> 角色</label>
<select
value={currentUser.role_id}
onChange={(e) => handleInputChange('role_id', parseInt(e.target.value))}
>
{roles.map(role => (
<option key={role.role_id} value={role.role_id}>{role.role_name}</option>
))}
</select>
</div>
{!isEditing && (
<div className="info-note">
<p>新用户的默认密码为系统配置的默认密码用户可登录后自行修改</p>
</div>
)}
</>
)}
</FormModal>
{/* 删除用户确认对话框 */}
<ConfirmDialog
isOpen={!!deleteConfirmInfo}
onClose={() => setDeleteConfirmInfo(null)}
onConfirm={handleDeleteUser}
title="删除用户"
message={`确定要删除用户"${deleteConfirmInfo?.caption}"吗?此操作无法撤销。`}
confirmText="确定删除"
cancelText="取消"
type="danger"
/>
{/* 重置密码确认对话框 */}
<ConfirmDialog
isOpen={!!resetConfirmInfo}
onClose={() => setResetConfirmInfo(null)}
onConfirm={handleResetPassword}
title="重置密码"
message={`确定要重置用户"${resetConfirmInfo?.caption}"的密码吗?重置后密码将恢复为系统默认密码。`}
confirmText="确定重置"
cancelText="取消"
type="warning"
/>
{/* Toast notifications */}
{toasts.map(toast => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)}
/>
))}
</div>
);
};
export default UserManagement;