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

507 lines
17 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, 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;