filesend/backend/app/uploads/a692440ca4c2448b93ac3d88f78b19a8_UserManagement.js

641 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;