filesend/backend/app/uploads/a692440ca4c2448b93ac3d88f78b19a8_UserManagement.js

641 lines
22 KiB
JavaScript
Raw Normal View History

2025-10-10 17:25:29 +08:00
import React, { useState, useEffect, useCallback } from 'react';
import axios from '../../services/axios';
import { adminAPI } from '../../services/api';
import { toast } from 'react-toastify';
import Pagination from '../../components/Pagination';
import '../../styles/Pagination.css';
import { debounce } from '../../utils/debounce';
const UserManagement = () => {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [totalPages, setTotalPages] = useState(1);
const [totalItems, setTotalItems] = useState(0);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('created_at');
const [sortOrder, setSortOrder] = useState('desc');
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [currentLoginUser, setCurrentLoginUser] = useState(null);
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
is_active: true,
daily_limit: 100
});
const [selectedUsers, setSelectedUsers] = useState([]);
const [showBatchModal, setShowBatchModal] = useState(false);
const [batchAction, setBatchAction] = useState('');
// 获取用户列表
const fetchUsers = async (reset = false) => {
try {
setLoading(true);
const currentPage = reset ? 1 : page;
if (reset) setPage(1);
const response = await axios.get('/api/admin/users', {
params: {
page: currentPage,
per_page: itemsPerPage,
search: searchQuery,
sort_by: sortBy,
sort_order: sortOrder
}
});
const { users: newUsers, total_pages, total_users } = response.data;
setUsers(newUsers);
setTotalPages(total_pages);
setTotalItems(total_users);
} catch (error) {
console.error('获取用户列表失败:', error);
toast.error('获取用户列表失败');
} finally {
setLoading(false);
}
};
const handlePageChange = (newPage) => {
setPage(newPage);
};
const handleItemsPerPageChange = (newItemsPerPage) => {
setItemsPerPage(newItemsPerPage);
setPage(1);
};
// 防抖搜索函数
const debouncedSearch = useCallback(
debounce(() => {
fetchUsers(true);
}, 500),
[]
);
useEffect(() => {
fetchCurrentUser();
}, []);
useEffect(() => {
debouncedSearch();
}, [searchQuery, debouncedSearch]);
useEffect(() => {
fetchUsers();
}, [sortBy, sortOrder, itemsPerPage, page]);
// 获取当前登录用户信息
const fetchCurrentUser = async () => {
try {
const res = await axios.get('/api/auth/me');
setCurrentLoginUser(res.data);
} catch (err) {
console.error('获取当前用户信息失败', err);
}
};
// 处理表单变化
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
};
// 打开添加用户模态框
const handleAddUser = () => {
setFormData({
username: '',
email: '',
password: '',
is_active: false,
daily_quota: 5
});
setShowAddModal(true);
};
// 打开编辑用户模态框
const handleEditUser = (user) => {
setCurrentUser(user);
setFormData({
username: user.username,
email: user.email,
password: '', // 不显示现有密码
is_active: user.is_active,
daily_quota: user.daily_quota
});
setShowEditModal(true);
};
// 提交添加用户
const submitAddUser = async () => {
try {
if (!formData.username || !formData.password) {
toast.warning('用户名和密码为必填项');
return;
}
await axios.post('/api/admin/users', formData);
toast.success('用户添加成功');
setShowAddModal(false);
fetchUsers();
} catch (err) {
toast.error('添加用户失败,可能用户名已存在');
console.error(err);
}
};
// 提交编辑用户
const submitEditUser = async () => {
try {
if (!currentUser) return;
// 准备要发送的数据,不包含空密码
const updateData = { ...formData };
if (!updateData.password) {
delete updateData.password;
}
await axios.put(`/api/admin/users/${currentUser.id}`, updateData);
toast.success('用户更新成功');
setShowEditModal(false);
fetchUsers();
} catch (err) {
toast.error('更新用户失败');
console.error(err);
}
};
// 删除用户
const handleDeleteUser = async (userId) => {
if (window.confirm('确定要删除这个用户吗?此操作不可恢复。')) {
try {
await adminAPI.deleteUser(userId);
toast.success('用户已删除');
fetchUsers();
} catch (err) {
toast.error('删除用户失败');
console.error(err);
}
}
};
// 处理用户选择
const handleSelectUser = (userId) => {
setSelectedUsers(prev =>
prev.includes(userId)
? prev.filter(id => id !== userId)
: [...prev, userId]
);
};
// 全选/取消全选
const handleSelectAll = () => {
if (selectedUsers.length === users.length) {
setSelectedUsers([]);
} else {
setSelectedUsers(users.map(user => user.id));
}
};
// 批量操作
const handleBatchAction = (action) => {
if (selectedUsers.length === 0) {
toast.warning('请先选择用户');
return;
}
setBatchAction(action);
setShowBatchModal(true);
};
// 执行批量操作
const executeBatchAction = async () => {
try {
let successCount = 0;
if (batchAction === 'delete') {
if (!window.confirm(`确定要删除选中的 ${selectedUsers.length} 个用户吗?此操作不可恢复。`)) {
return;
}
for (const userId of selectedUsers) {
try {
await adminAPI.deleteUser(userId);
successCount++;
} catch (err) {
console.error(`删除用户 ${userId} 失败:`, err);
}
}
toast.success(`成功删除 ${successCount}/${selectedUsers.length} 个用户`);
} else if (batchAction === 'activate') {
for (const userId of selectedUsers) {
try {
await adminAPI.updateUser(userId, { is_active: true });
successCount++;
} catch (err) {
console.error(`激活用户 ${userId} 失败:`, err);
}
}
toast.success(`成功激活 ${successCount}/${selectedUsers.length} 个用户`);
} else if (batchAction === 'deactivate') {
for (const userId of selectedUsers) {
try {
await adminAPI.updateUser(userId, { is_active: false });
successCount++;
} catch (err) {
console.error(`禁用用户 ${userId} 失败:`, err);
}
}
toast.success(`成功禁用 ${successCount}/${selectedUsers.length} 个用户`);
}
setShowBatchModal(false);
setSelectedUsers([]);
fetchUsers();
} catch (err) {
toast.error('批量操作失败');
console.error(err);
}
};
// 切换用户激活状态
const toggleUserStatus = async (userId, currentStatus) => {
try {
await axios.put(`/api/admin/users/${userId}`, {
is_active: !currentStatus
});
toast.success(`用户已${!currentStatus ? '激活' : '禁用'}`);
fetchUsers();
} catch (err) {
toast.error('更新用户状态失败');
console.error(err);
}
};
if (loading && page === 1) {
return (
<div className="d-flex justify-content-center align-items-center vh-100">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
<h1>用户管理</h1>
<div className="flex items-center space-x-4">
<input
type="text"
placeholder="搜索用户名或邮箱..."
className="p-2 border border-gray-300 rounded-md w-64 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<select
className="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="created_at">按创建时间排序</option>
<option value="username">按用户名排序</option>
</select>
<select
className="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
>
<option value="desc">降序</option>
<option value="asc">升序</option>
</select>
{selectedUsers.length > 0 && (
<>
<button
className="btn-admin-outline-success btn-sm me-2"
onClick={() => handleBatchAction('activate')}
>
批量激活
</button>
<button
className="btn-admin-outline-warning btn-sm me-2"
onClick={() => handleBatchAction('deactivate')}
>
批量禁用
</button>
<button
className="btn-admin-outline-danger btn-sm me-2"
onClick={() => handleBatchAction('delete')}
>
批量删除
</button>
</>
)}
<button className="btn-admin-primary" onClick={handleAddUser}>
<i className="bi bi-plus-circle me-2"></i>
</button>
</div>
</div>
{selectedUsers.length > 0 && (
<div className="alert alert-info d-flex justify-content-between align-items-center mb-3">
<span>已选择 {selectedUsers.length} 个用户</span>
</div>
)}
<div className="card">
<div className="card-body">
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead>
<tr>
<th>
<input
type="checkbox"
checked={selectedUsers.length === users.length && users.length > 0}
ref={(el) => {
if (el) {
el.indeterminate = selectedUsers.length > 0 && selectedUsers.length < users.length;
}
}}
onChange={handleSelectAll}
/>
</th>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>状态</th>
<th>每日限额</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.length === 0 ? (
<tr>
<td colSpan="8" className="text-center">暂无用户数据</td>
</tr>
) : (
users.map(user => (
<tr key={user.id}>
<td>
<input
type="checkbox"
checked={selectedUsers.includes(user.id)}
onChange={() => handleSelectUser(user.id)}
/>
</td>
<td>{user.id}</td>
<td>{user.username}</td>
<td>{user.email}</td>
<td>
<span className={`badge ${user.is_active ? 'bg-success' : 'bg-danger'}`}>
{user.is_active ? '已激活' : '未激活'}
</span>
</td>
<td>{user.daily_quota}</td>
<td>{new Date(user.created_at).toLocaleString()}</td>
<td>
<div className="admin-btn-group">
<button
className="btn-admin-outline-primary btn-sm"
onClick={() => handleEditUser(user)}
>
<i className="bi bi-pencil me-1"></i>
</button>
<button
className={user.is_active ? "btn-admin-outline-danger btn-sm" : "btn-admin-outline-success btn-sm"}
onClick={() => toggleUserStatus(user.id, user.is_active)}
>
<i className={`bi bi-${user.is_active ? 'x-circle' : 'check-circle'} me-1`}></i>
{user.is_active ? '禁用' : '激活'}
</button>
<button
className="btn-admin-outline-danger btn-sm"
onClick={() => handleDeleteUser(user.id)}
disabled={currentLoginUser && user.id === currentLoginUser.id} // 不能删除自己
>
<i className="bi bi-trash me-1"></i>
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<Pagination
currentPage={page}
totalPages={totalPages}
itemsPerPage={itemsPerPage}
onItemsPerPageChange={handleItemsPerPageChange}
onPageChange={handlePageChange}
totalItems={totalItems}
loading={loading}
/>
</div>
</div>
{/* 添加用户模态框 */}
{showAddModal && (
<div className="modal-overlay" onClick={() => setShowAddModal(false)}>
<div className="admin-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h5 className="modal-title">添加新用户</h5>
<button className="modal-close" onClick={() => setShowAddModal(false)}>×</button>
</div>
<div className="modal-body">
<form>
<div className="mb-3">
<label className="form-label">用户名</label>
<input
type="text"
className="form-control"
name="username"
value={formData.username}
onChange={handleChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label">邮箱</label>
<input
type="email"
className="form-control"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div className="mb-3">
<label className="form-label">密码</label>
<input
type="password"
className="form-control"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label">每日领取限额</label>
<input
type="number"
className="form-control"
name="daily_quota"
value={formData.daily_quota}
onChange={handleChange}
min="1"
required
/>
</div>
<div className="mb-3 form-check">
<input
type="checkbox"
className="form-check-input"
name="is_active"
checked={formData.is_active}
onChange={handleChange}
/>
<label className="form-check-label">是否激活</label>
</div>
</form>
</div>
<div className="modal-footer">
<button className="btn-admin-outline-secondary" onClick={() => setShowAddModal(false)}>
取消
</button>
<button className="btn-admin-outline-primary" onClick={submitAddUser}>
添加
</button>
</div>
</div>
</div>
)}
{/* 编辑用户模态框 */}
{showEditModal && (
<div className="modal-overlay" onClick={() => setShowEditModal(false)}>
<div className="admin-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h5 className="modal-title">编辑用户</h5>
<button className="modal-close" onClick={() => setShowEditModal(false)}>×</button>
</div>
<div className="modal-body">
<form>
<div className="mb-3">
<label className="form-label">用户名</label>
<input
type="text"
className="form-control"
name="username"
value={formData.username}
onChange={handleChange}
required
disabled={currentUser?.is_admin} // 管理员用户名不可修改
/>
</div>
<div className="mb-3">
<label className="form-label">邮箱</label>
<input
type="email"
className="form-control"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div className="mb-3">
<label className="form-label">密码不修改请留空</label>
<input
type="password"
className="form-control"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="不修改请留空"
/>
</div>
<div className="mb-3">
<label className="form-label">每日领取限额</label>
<input
type="number"
className="form-control"
name="daily_quota"
value={formData.daily_quota}
onChange={handleChange}
min="1"
required
/>
</div>
<div className="mb-3 form-check">
<input
type="checkbox"
className="form-check-input"
name="is_active"
checked={formData.is_active}
onChange={handleChange}
/>
<label className="form-check-label">是否激活</label>
</div>
</form>
</div>
<div className="modal-footer">
<button className="btn-admin-outline-secondary" onClick={() => setShowEditModal(false)}>
取消
</button>
<button className="btn-admin-outline-primary" onClick={submitEditUser}>
保存
</button>
</div>
</div>
</div>
)}
{/* 批量操作确认模态框 */}
{showBatchModal && (
<div className="modal-overlay" onClick={() => setShowBatchModal(false)}>
<div className="admin-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h5 className="modal-title">确认批量操作</h5>
<button className="modal-close" onClick={() => setShowBatchModal(false)}>×</button>
</div>
<div className="modal-body">
{batchAction === 'delete' && (
<p>确定要删除选中的 {selectedUsers.length} 个用户吗此操作不可恢复</p>
)}
{batchAction === 'activate' && (
<p>确定要激活选中的 {selectedUsers.length} 个用户吗</p>
)}
{batchAction === 'deactivate' && (
<p>确定要禁用选中的 {selectedUsers.length} 个用户吗</p>
)}
</div>
<div className="modal-footer">
<button className="btn-admin-outline-secondary" onClick={() => setShowBatchModal(false)}>
取消
</button>
<button className="btn-admin-outline-primary" onClick={executeBatchAction}>
确认
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default UserManagement;