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

368 lines
12 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, { useState, useEffect } from 'react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
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">
<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;