368 lines
12 KiB
JavaScript
368 lines
12 KiB
JavaScript
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; |