filesend/backend/app/uploads/66c8e2fd94844e8fbc25e32472fbf0e0_CategoryPermissionManagement.js

507 lines
17 KiB
JavaScript
Raw Normal View History

2025-10-10 17:25:29 +08:00
import React, { useState, useEffect, useRef } from 'react';
import axios from '../../services/axios';
import { toast } from 'react-toastify';
// 添加全局样式
const globalStyles = {
modalOpen: {
overflow: 'hidden',
},
cursorPointer: {
cursor: 'pointer',
},
pageContainer: {
padding: '20px',
backgroundColor: '#f8f9fa',
minHeight: 'calc(100vh - 56px)',
},
};
const CategoryPermissionManagement = () => {
const [users, setUsers] = useState([]);
const [categories, setCategories] = useState([]);
const [permissions, setPermissions] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState(null);
const [selectedCategories, setSelectedCategories] = useState([]);
const [inheritToChildren, setInheritToChildren] = useState(true);
const [showPermissionModal, setShowPermissionModal] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const searchTimeoutRef = useRef(null);
// 获取用户列表
const fetchUsers = async (search = '') => {
try {
const params = {};
if (search) {
params.search = search;
}
const res = await axios.get('/api/admin/users', { params });
if (search) {
setSearchResults(res.data.users);
} else {
setUsers(res.data.users);
}
} catch (err) {
toast.error('获取用户列表失败');
console.error(err);
}
};
// 获取分类列表
const fetchCategories = async () => {
try {
const res = await axios.get('/api/admin/categories');
// 检查返回的数据结构
console.log('API返回的分类数据:', res.data);
// 确保categories是一个数组
let categoriesData = [];
if (res.data && res.data.categories) {
// 如果API返回的是 {categories: [...]} 格式
categoriesData = res.data.categories || [];
} else if (Array.isArray(res.data)) {
// 如果API直接返回数组
categoriesData = res.data;
} else {
console.error('API返回的分类数据格式不正确:', res.data);
categoriesData = [];
}
// 确保每个分类对象都有id和name属性
const formattedCategories = categoriesData.map(cat => {
return {
...cat,
id: cat.id || cat._id || cat.category_id || '',
name: cat.name || cat.category_name || '未命名分类',
children: cat.children || []
};
});
setCategories(formattedCategories);
} catch (err) {
toast.error('获取分类列表失败');
console.error(err);
setCategories([]);
}
};
// 获取权限列表
const fetchPermissions = async (userId = null) => {
try {
setLoading(true);
const params = {};
if (userId) {
params.user_id = userId;
}
const res = await axios.get('/api/admin/category-permissions', { params });
setPermissions(res.data.permissions);
} catch (err) {
toast.error('获取权限列表失败');
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
fetchCategories();
fetchPermissions();
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, []);
// 处理搜索输入变化
const handleSearchChange = (e) => {
const query = e.target.value;
setSearchQuery(query);
// 清除之前的定时器
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (query.trim()) {
searchTimeoutRef.current = setTimeout(() => {
setIsSearching(true);
fetchUsers(query);
}, 300);
} else {
setIsSearching(false);
setSearchResults([]);
}
};
// 清除搜索
const clearSearch = () => {
setSearchQuery('');
setIsSearching(false);
setSearchResults([]);
};
// 选择用户时获取该用户的权限
const handleUserSelect = (userId) => {
setSelectedUser(userId);
fetchPermissions(userId);
};
// 打开权限分配模态框
const handleOpenPermissionModal = (userId) => {
setSelectedUser(userId);
setSelectedCategories([]);
setInheritToChildren(true);
setShowPermissionModal(true);
};
// 处理分类选择
const handleCategorySelect = (categoryId) => {
if (selectedCategories.includes(categoryId)) {
setSelectedCategories(selectedCategories.filter(id => id !== categoryId));
} else {
setSelectedCategories([...selectedCategories, categoryId]);
}
};
// 批量分配权限
const handleBatchAssignPermissions = async () => {
if (!selectedUser || selectedCategories.length === 0) {
toast.error('请选择用户和至少一个分类');
return;
}
try {
const res = await axios.post(`/api/admin/users/${selectedUser}/category-permissions/batch`, {
category_ids: selectedCategories,
inherit_to_children: inheritToChildren
});
toast.success(res.data.message);
setShowPermissionModal(false);
fetchPermissions(selectedUser);
} catch (err) {
toast.error('分配权限失败: ' + (err.response?.data?.message || err.message));
console.error(err);
}
};
// 删除权限
const handleDeletePermission = async (permissionId) => {
if (!window.confirm('确定要删除此权限吗?')) {
return;
}
try {
await axios.delete(`/api/admin/category-permissions/${permissionId}`);
toast.success('权限删除成功');
fetchPermissions(selectedUser);
} catch (err) {
toast.error('删除权限失败');
console.error(err);
}
};
// 更新权限继承设置
const handleUpdatePermission = async (permissionId, inheritToChildren) => {
try {
await axios.put(`/api/admin/category-permissions/${permissionId}`, {
inherit_to_children: inheritToChildren
});
toast.success('权限更新成功');
fetchPermissions(selectedUser);
} catch (err) {
toast.error('更新权限失败');
console.error(err);
}
};
// 递归渲染分类树
const renderCategoryTree = (categories, level = 0) => {
return categories.map(category => (
<div key={category.id}>
<div
className="category-item d-flex align-items-center py-1"
style={{ paddingLeft: `${level * 20}px` }}
>
<div className="form-check mb-0">
<input
className="form-check-input"
type="checkbox"
id={`category-${category.id}`}
checked={selectedCategories.includes(category.id)}
onChange={() => handleCategorySelect(category.id)}
/>
<label
className="form-check-label"
htmlFor={`category-${category.id}`}
style={{ cursor: 'pointer' }}
>
{category.name}
</label>
</div>
</div>
{category.children && category.children.length > 0 && (
renderCategoryTree(category.children, level + 1)
)}
</div>
));
};
// 获取分类名称
const getCategoryName = (categoryId) => {
const findCategory = (categories, id) => {
for (const category of categories) {
if (category.id === id) {
return category.name;
}
if (category.children && category.children.length > 0) {
const found = findCategory(category.children, id);
if (found) return found;
}
}
return '未知分类';
};
return findCategory(categories, categoryId);
};
// 获取用户名称
const getUserName = (userId) => {
const user = users.find(u => u.id === userId);
return user ? user.username : '未知用户';
};
// 当模态框打开时应用全局样式
useEffect(() => {
if (showPermissionModal) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [showPermissionModal]);
return (
<div className="container-fluid" style={globalStyles.pageContainer}>
<div className="card shadow-sm mb-4">
<div className="card-header bg-primary text-white">
<h2 className="mb-0">分类权限管理</h2>
</div>
<div className="card-body">
<div className="row mb-4">
<div className="col-md-6">
<div className="card shadow-sm">
<div className="card-header bg-light">
<h3 className="h5 mb-0">用户列表</h3>
</div>
<div className="card-body">
<div className="mb-3">
<div className="input-group">
<input
type="text"
className="form-control"
placeholder="搜索用户名或邮箱..."
value={searchQuery}
onChange={handleSearchChange}
/>
{searchQuery && (
<button
className="btn btn-outline-secondary"
type="button"
onClick={clearSearch}
>
<i className="bi bi-x"></i>
</button>
)}
</div>
</div>
<div className="list-group" style={{ maxHeight: '400px', overflowY: 'auto' }}>
{(isSearching ? searchResults : users).map(user => (
<div
key={user.id}
className={`list-group-item list-group-item-action d-flex justify-content-between align-items-center ${selectedUser === user.id ? 'active' : ''}`}
onClick={() => handleUserSelect(user.id)}
style={{ cursor: 'pointer' }}
>
<div>
<div>{user.username}</div>
<small className="text-muted">{user.email}</small>
</div>
<button
className="btn btn-sm btn-primary"
onClick={(e) => {
e.stopPropagation();
handleOpenPermissionModal(user.id);
}}
>
分配权限
</button>
</div>
))}
{(isSearching ? searchResults : users).length === 0 && (
<div className="list-group-item text-center text-muted">
{isSearching ? '未找到匹配的用户' : '暂无用户数据'}
</div>
)}
</div>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card">
<div className="card-header bg-primary text-white">权限列表</div>
<div className="card-body">
{loading ? (
<div className="d-flex justify-content-center">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">加载中...</span>
</div>
</div>
) : permissions.length > 0 ? (
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead className="thead-light">
<tr>
<th>用户</th>
<th>分类</th>
<th>继承子分类</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{permissions.map(permission => (
<tr key={permission.id}>
<td>{getUserName(permission.user_id)}</td>
<td>{getCategoryName(permission.category_id)}</td>
<td>
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={permission.inherit_to_children}
onChange={() => handleUpdatePermission(permission.id, !permission.inherit_to_children)}
/>
</div>
</td>
<td>
<button
className="btn btn-sm btn-danger"
onClick={() => handleDeletePermission(permission.id)}
title="删除权限"
>
<i className="bi bi-trash"></i>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="alert alert-info">暂无权限数据</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
{/* 权限分配模态框 */}
{showPermissionModal && (
<>
<div className="modal show d-block" tabIndex="-1" style={{ zIndex: 1050 }}>
<div className="modal-dialog modal-lg">
<div className="modal-content">
<div className="modal-header bg-primary text-white">
<h5 className="modal-title"> {getUserName(selectedUser)} 分配分类权限</h5>
<button
type="button"
className="btn-close btn-close-white"
onClick={() => setShowPermissionModal(false)}
></button>
</div>
<div className="modal-body">
<div className="row">
<div className="col-md-12 mb-3">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
id="inheritToChildren"
checked={inheritToChildren}
onChange={() => setInheritToChildren(!inheritToChildren)}
/>
<label className="form-check-label" htmlFor="inheritToChildren">
权限继承到子分类
</label>
<small className="form-text text-muted d-block">
启用后授予父分类权限将自动继承所有子分类权限
</small>
</div>
</div>
<div className="col-md-12">
<div className="card">
<div className="card-header bg-light">选择分类</div>
<div className="card-body p-0">
<div className="category-tree p-3" style={{ maxHeight: '300px', overflowY: 'auto' }}>
{renderCategoryTree(categories)}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowPermissionModal(false)}
>
<i className="bi bi-x-circle"></i>
</button>
<button
type="button"
className="btn btn-primary"
onClick={handleBatchAssignPermissions}
disabled={selectedCategories.length === 0}
>
<i className="bi bi-check-circle"></i>
</button>
</div>
</div>
</div>
</div>
<div
className="modal-backdrop fade show"
style={{ zIndex: 1040 }}
onClick={() => setShowPermissionModal(false)}
></div>
</>
)}
{/* 添加全局样式,防止模态框打开时页面滚动 */}
{showPermissionModal && (
<style>{`
body {
overflow: hidden !important;
padding-right: 15px; /* 防止滚动条消失导致页面抖动 */
}
`}</style>
)}
</div>
);
};
export default CategoryPermissionManagement;