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