641 lines
22 KiB
JavaScript
641 lines
22 KiB
JavaScript
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;
|