2026-01-19 11:03:08 +00:00
|
|
|
|
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';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
import ConfirmDialog from '../../components/ConfirmDialog';
|
|
|
|
|
|
import FormModal from '../../components/FormModal';
|
|
|
|
|
|
import Toast from '../../components/Toast';
|
|
|
|
|
|
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 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
<button className="btn btn-primary" onClick={() => handleOpenModal()}><Plus size={16} /> 新增用户</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{loading && <p>加载中...</p>}
|
|
|
|
|
|
{error && <p className="error-message">{error}</p>}
|
|
|
|
|
|
{!loading && !error && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<table className="users-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>ID</th>
|
|
|
|
|
|
<th>用户名</th>
|
|
|
|
|
|
<th>姓名</th>
|
|
|
|
|
|
<th>邮箱</th>
|
|
|
|
|
|
<th>角色</th>
|
|
|
|
|
|
<th>创建时间</th>
|
|
|
|
|
|
<th>操作</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{users.map(user => (
|
|
|
|
|
|
<tr key={user.user_id}>
|
|
|
|
|
|
<td>{user.user_id}</td>
|
|
|
|
|
|
<td>{user.username}</td>
|
|
|
|
|
|
<td>{user.caption}</td>
|
|
|
|
|
|
<td>{user.email}</td>
|
|
|
|
|
|
<td>{user.role_name}</td>
|
|
|
|
|
|
<td>{new Date(user.created_at).toLocaleString()}</td>
|
|
|
|
|
|
<td className="action-cell">
|
|
|
|
|
|
<button className="action-btn" onClick={() => handleOpenModal(user)} title="修改"><Edit size={16} />修改</button>
|
|
|
|
|
|
<button className="action-btn btn-danger" onClick={() => openDeleteConfirm(user)} title="删除"><Trash2 size={16} />删除</button>
|
|
|
|
|
|
<button className="action-btn btn-warning" onClick={() => openResetConfirm(user)} title="重置密码"><KeyRound size={16} />重置</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<div className="pagination">
|
|
|
|
|
|
<span>总计 {total} 条</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>上一页</button>
|
|
|
|
|
|
<span>第 {page} 页 / {Math.ceil(total / pageSize)}</span>
|
|
|
|
|
|
<button onClick={() => setPage(p => p + 1)} disabled={page * pageSize >= total}>下一页</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 用户表单模态框 */}
|
|
|
|
|
|
<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;
|