2025-03-13 10:24:05 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { useDispatch } from 'react-redux';
|
|
|
|
|
import { get, put } from '../../../services/api';
|
|
|
|
|
import { showNotification } from '../../../store/notification.slice';
|
|
|
|
|
import UserPermissionDetails from './UserPermissionDetails';
|
2025-03-20 01:06:52 +08:00
|
|
|
|
import './UserPermissions.css';
|
|
|
|
|
import SvgIcon from '../../../components/SvgIcon';
|
|
|
|
|
|
|
|
|
|
// 模拟数据
|
|
|
|
|
const mockUsers = [
|
|
|
|
|
{
|
|
|
|
|
id: '1',
|
|
|
|
|
username: 'zhangsan',
|
|
|
|
|
name: '张三',
|
|
|
|
|
department: '达人组',
|
|
|
|
|
position: '达人对接',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 1,
|
|
|
|
|
edit: 0,
|
|
|
|
|
admin: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '2',
|
|
|
|
|
username: 'lisi',
|
|
|
|
|
name: '李四',
|
|
|
|
|
department: '人力资源组',
|
|
|
|
|
position: 'HR',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 1,
|
|
|
|
|
edit: 0,
|
|
|
|
|
admin: 2,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '3',
|
|
|
|
|
username: 'wangwu',
|
|
|
|
|
name: '王五',
|
|
|
|
|
department: '市场部',
|
|
|
|
|
position: '市场专员',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 2,
|
|
|
|
|
edit: 1,
|
|
|
|
|
admin: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '4',
|
|
|
|
|
username: 'zhaoliu',
|
|
|
|
|
name: '赵六',
|
|
|
|
|
department: '技术部',
|
|
|
|
|
position: '前端开发',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 3,
|
|
|
|
|
edit: 2,
|
|
|
|
|
admin: 1,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '5',
|
|
|
|
|
username: 'sunqi',
|
|
|
|
|
name: '孙七',
|
|
|
|
|
department: '产品部',
|
|
|
|
|
position: '产品经理',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 4,
|
|
|
|
|
edit: 2,
|
|
|
|
|
admin: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '6',
|
|
|
|
|
username: 'zhouba',
|
|
|
|
|
name: '周八',
|
|
|
|
|
department: '设计部',
|
|
|
|
|
position: 'UI设计师',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 1,
|
|
|
|
|
edit: 1,
|
|
|
|
|
admin: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '7',
|
|
|
|
|
username: 'wujiu',
|
|
|
|
|
name: '吴九',
|
|
|
|
|
department: '财务部',
|
|
|
|
|
position: '财务主管',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 2,
|
|
|
|
|
edit: 0,
|
|
|
|
|
admin: 3,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '8',
|
|
|
|
|
username: 'zhengshi',
|
|
|
|
|
name: '郑十',
|
|
|
|
|
department: '行政部',
|
|
|
|
|
position: '行政专员',
|
|
|
|
|
permissions_count: {
|
|
|
|
|
read: 1,
|
|
|
|
|
edit: 0,
|
|
|
|
|
admin: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 每页显示选项
|
|
|
|
|
const PAGE_SIZE_OPTIONS = [5, 10, 15, 20];
|
2025-03-13 10:24:05 +08:00
|
|
|
|
|
|
|
|
|
export default function UserPermissions() {
|
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
const [users, setUsers] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState(null);
|
|
|
|
|
const [selectedUser, setSelectedUser] = useState(null);
|
|
|
|
|
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
2025-03-20 01:06:52 +08:00
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
|
|
|
|
|
// 分页状态
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
const [pageSize, setPageSize] = useState(5);
|
|
|
|
|
const [totalPages, setTotalPages] = useState(1);
|
2025-03-13 10:24:05 +08:00
|
|
|
|
|
|
|
|
|
// 获取用户列表
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchUsers = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const response = await get('/users/permissions/');
|
|
|
|
|
|
|
|
|
|
if (response && response.code === 200) {
|
2025-03-20 01:06:52 +08:00
|
|
|
|
// 只有当API返回的用户列表为空时才使用模拟数据
|
|
|
|
|
const apiUsers = response.data.users || [];
|
|
|
|
|
if (apiUsers.length > 0) {
|
|
|
|
|
setUsers(apiUsers);
|
|
|
|
|
} else {
|
|
|
|
|
console.log('API返回的用户列表为空,使用模拟数据');
|
|
|
|
|
setUsers(mockUsers);
|
|
|
|
|
}
|
2025-03-13 10:24:05 +08:00
|
|
|
|
} else {
|
2025-03-20 01:06:52 +08:00
|
|
|
|
// API请求失败,使用模拟数据
|
|
|
|
|
console.log('API请求失败,使用模拟数据');
|
|
|
|
|
setUsers(mockUsers);
|
2025-03-13 10:24:05 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取用户列表失败:', error);
|
|
|
|
|
setError('获取用户列表失败');
|
2025-03-20 01:06:52 +08:00
|
|
|
|
// API请求出错,使用模拟数据作为后备
|
|
|
|
|
console.log('API请求出错,使用模拟数据作为后备');
|
|
|
|
|
setUsers(mockUsers);
|
2025-03-13 10:24:05 +08:00
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-03-20 01:06:52 +08:00
|
|
|
|
// 当用户列表或搜索词变化时,更新总页数
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const filteredUsers = getFilteredUsers();
|
|
|
|
|
setTotalPages(Math.ceil(filteredUsers.length / pageSize));
|
|
|
|
|
// 如果当前页超出了新的总页数,则重置为第一页
|
|
|
|
|
if (currentPage > Math.ceil(filteredUsers.length / pageSize)) {
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
}
|
|
|
|
|
}, [users, searchTerm, pageSize]);
|
|
|
|
|
|
2025-03-13 10:24:05 +08:00
|
|
|
|
// 打开用户权限详情弹窗
|
|
|
|
|
const handleOpenDetailsModal = (user) => {
|
|
|
|
|
setSelectedUser(user);
|
|
|
|
|
setShowDetailsModal(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭用户权限详情弹窗
|
|
|
|
|
const handleCloseDetailsModal = () => {
|
|
|
|
|
setShowDetailsModal(false);
|
|
|
|
|
setSelectedUser(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 保存用户权限更改
|
|
|
|
|
const handleSavePermissions = async (userId, updatedPermissions) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await put(`/users/${userId}/permissions/`, {
|
|
|
|
|
permissions: updatedPermissions,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response && response.code === 200) {
|
|
|
|
|
// 更新用户列表中的权限信息
|
|
|
|
|
setUsers(
|
|
|
|
|
users.map((user) => {
|
|
|
|
|
if (user.id === userId) {
|
|
|
|
|
return {
|
|
|
|
|
...user,
|
|
|
|
|
permissions: {
|
|
|
|
|
...user.permissions,
|
|
|
|
|
...response.data.permissions,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return user;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
dispatch(
|
|
|
|
|
showNotification({
|
|
|
|
|
message: '权限更新成功',
|
|
|
|
|
type: 'success',
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 关闭弹窗
|
|
|
|
|
handleCloseDetailsModal();
|
|
|
|
|
} else {
|
|
|
|
|
dispatch(
|
|
|
|
|
showNotification({
|
|
|
|
|
message: '权限更新失败',
|
|
|
|
|
type: 'danger',
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('权限更新失败:', error);
|
|
|
|
|
dispatch(
|
|
|
|
|
showNotification({
|
|
|
|
|
message: '权限更新失败',
|
|
|
|
|
type: 'danger',
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-20 01:06:52 +08:00
|
|
|
|
// 处理搜索输入变化
|
|
|
|
|
const handleSearchChange = (e) => {
|
|
|
|
|
setSearchTerm(e.target.value);
|
|
|
|
|
setCurrentPage(1); // 重置为第一页
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理页码变化
|
|
|
|
|
const handlePageChange = (page) => {
|
|
|
|
|
setCurrentPage(page);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理每页显示数量变化
|
|
|
|
|
const handlePageSizeChange = (e) => {
|
|
|
|
|
const newPageSize = parseInt(e.target.value);
|
|
|
|
|
setPageSize(newPageSize);
|
|
|
|
|
setCurrentPage(1); // 重置为第一页
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 获取过滤后的用户列表
|
|
|
|
|
const getFilteredUsers = () => {
|
|
|
|
|
if (!searchTerm.trim()) return users;
|
|
|
|
|
|
|
|
|
|
return users.filter(
|
|
|
|
|
(user) =>
|
|
|
|
|
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
user.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
user.position.toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 获取当前页的数据
|
|
|
|
|
const getCurrentPageData = () => {
|
|
|
|
|
const filteredUsers = getFilteredUsers();
|
|
|
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
|
|
|
const endIndex = startIndex + pageSize;
|
|
|
|
|
return filteredUsers.slice(startIndex, endIndex);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 渲染分页控件
|
|
|
|
|
const renderPagination = () => {
|
|
|
|
|
if (totalPages <= 1) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className='d-flex justify-content-center align-items-center mt-4'>
|
|
|
|
|
<nav aria-label='用户权限分页'>
|
|
|
|
|
<ul className='pagination'>
|
|
|
|
|
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
|
|
|
|
<button
|
|
|
|
|
className='page-link'
|
|
|
|
|
onClick={() => handlePageChange(currentPage - 1)}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
>
|
|
|
|
|
上一页
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
<li className='page-item active'>
|
|
|
|
|
<span className='page-link'>
|
|
|
|
|
{currentPage} / {totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
|
|
|
|
<button
|
|
|
|
|
className='page-link'
|
|
|
|
|
onClick={() => handlePageChange(currentPage + 1)}
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
>
|
|
|
|
|
下一页
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<div className='ms-3'>
|
|
|
|
|
<select
|
|
|
|
|
className='form-select form-select-sm'
|
|
|
|
|
value={pageSize}
|
|
|
|
|
onChange={handlePageSizeChange}
|
|
|
|
|
aria-label='每页显示数量'
|
|
|
|
|
>
|
|
|
|
|
{PAGE_SIZE_OPTIONS.map((size) => (
|
|
|
|
|
<option key={size} value={size}>
|
|
|
|
|
{size}条/页
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-13 10:24:05 +08:00
|
|
|
|
// 渲染加载状态
|
2025-03-20 01:06:52 +08:00
|
|
|
|
if (loading && users.length === 0) {
|
2025-03-13 10:24:05 +08:00
|
|
|
|
return (
|
|
|
|
|
<div className='text-center py-5'>
|
|
|
|
|
<div className='spinner-border' role='status'>
|
|
|
|
|
<span className='visually-hidden'>加载中...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className='mt-3'>加载用户列表...</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染错误状态
|
2025-03-20 01:06:52 +08:00
|
|
|
|
if (error && users.length === 0) {
|
2025-03-13 10:24:05 +08:00
|
|
|
|
return (
|
|
|
|
|
<div className='alert alert-danger' role='alert'>
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染空状态
|
|
|
|
|
if (users.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className='alert alert-info' role='alert'>
|
|
|
|
|
暂无用户数据
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 01:06:52 +08:00
|
|
|
|
// 获取当前页的数据
|
|
|
|
|
const currentPageData = getCurrentPageData();
|
|
|
|
|
|
|
|
|
|
// 获取过滤后的总用户数
|
|
|
|
|
const filteredUsersCount = getFilteredUsers().length;
|
|
|
|
|
|
2025-03-13 10:24:05 +08:00
|
|
|
|
// 渲染用户列表
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2025-03-20 01:06:52 +08:00
|
|
|
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
|
|
|
|
<h5 className='mb-0'>用户权限管理</h5>
|
|
|
|
|
<div className='input-group' style={{ maxWidth: '250px' }}>
|
|
|
|
|
<input
|
|
|
|
|
type='text'
|
|
|
|
|
className='form-control'
|
|
|
|
|
placeholder='搜索用户...'
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
|
/>
|
|
|
|
|
<span className='input-group-text'>
|
|
|
|
|
<i className='bi bi-search'></i>
|
|
|
|
|
</span>
|
2025-03-13 10:24:05 +08:00
|
|
|
|
</div>
|
2025-03-20 01:06:52 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{filteredUsersCount > 0 ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className='card'>
|
|
|
|
|
<div className='table-responsive'>
|
|
|
|
|
<table className='table table-hover mb-0'>
|
|
|
|
|
<thead className='table-light'>
|
|
|
|
|
<tr>
|
|
|
|
|
<th scope='col'>用户</th>
|
|
|
|
|
<th scope='col'>部门</th>
|
|
|
|
|
<th scope='col'>职位</th>
|
|
|
|
|
<th scope='col'>数据集权限</th>
|
|
|
|
|
<th scope='col' className='text-end'>
|
|
|
|
|
操作
|
|
|
|
|
</th>
|
2025-03-13 10:24:05 +08:00
|
|
|
|
</tr>
|
2025-03-20 01:06:52 +08:00
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{currentPageData.map((user) => (
|
|
|
|
|
<tr key={user.id}>
|
|
|
|
|
<td>
|
|
|
|
|
<div className='d-flex align-items-center'>
|
|
|
|
|
<div
|
|
|
|
|
className='bg-dark rounded-circle text-white d-flex align-items-center justify-content-center me-2'
|
|
|
|
|
style={{ width: '36px', height: '36px' }}
|
|
|
|
|
>
|
|
|
|
|
{user.name.charAt(0)}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className='fw-medium'>{user.name}</div>
|
|
|
|
|
<div className='text-muted small'>{user.username}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className='align-middle'>{user.department}</td>
|
|
|
|
|
<td className='align-middle'>{user.position}</td>
|
|
|
|
|
<td className='align-middle'>
|
|
|
|
|
{user.permissions_count && (
|
|
|
|
|
<div className='d-flex flex-wrap gap-1'>
|
|
|
|
|
{user.permissions_count.read > 0 && (
|
|
|
|
|
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
|
|
|
|
|
完全访问: {user.permissions_count.read}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{user.permissions_count.edit > 0 && (
|
|
|
|
|
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
|
|
|
|
|
只读访问: {user.permissions_count.edit}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{user.permissions_count.admin > 0 && (
|
|
|
|
|
<span className='badge bg-danger d-flex align-items-center gap-1'>
|
|
|
|
|
无访问权限: {user.permissions_count.admin}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td className='text-end align-middle'>
|
|
|
|
|
<button
|
|
|
|
|
className='btn btn-outline-dark btn-sm'
|
|
|
|
|
onClick={() => handleOpenDetailsModal(user)}
|
|
|
|
|
>
|
|
|
|
|
修改权限
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2025-03-13 10:24:05 +08:00
|
|
|
|
</div>
|
2025-03-20 01:06:52 +08:00
|
|
|
|
|
|
|
|
|
{/* 分页控件 */}
|
|
|
|
|
{renderPagination()}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<div className='alert alert-info' role='alert'>
|
|
|
|
|
没有找到匹配的用户
|
2025-03-13 10:24:05 +08:00
|
|
|
|
</div>
|
2025-03-20 01:06:52 +08:00
|
|
|
|
)}
|
2025-03-13 10:24:05 +08:00
|
|
|
|
|
|
|
|
|
{/* 用户权限详情弹窗 */}
|
|
|
|
|
{showDetailsModal && selectedUser && (
|
|
|
|
|
<UserPermissionDetails
|
|
|
|
|
user={selectedUser}
|
|
|
|
|
onClose={handleCloseDetailsModal}
|
|
|
|
|
onSave={handleSavePermissions}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|