[dev]permissions page

This commit is contained in:
susie-laptop 2025-03-24 21:47:30 -04:00
parent 1ba460b4cf
commit cc0f56a5d4
11 changed files with 353 additions and 294 deletions

View File

@ -25,14 +25,14 @@ function App() {
// WebSocket // WebSocket
if (user && !isConnected) { if (user && !isConnected) {
initWebSocket() // initWebSocket()
.then(() => { // .then(() => {
// dispatch(setWebSocketConnected(true)); // dispatch(setWebSocketConnected(true));
// console.log('WebSocket connection initialized'); // console.log('WebSocket connection initialized');
}) // })
.catch((error) => { // .catch((error) => {
console.error('Failed to initialize WebSocket connection:', error); // console.error('Failed to initialize WebSocket connection:', error);
}); // });
} }
// WebSocket // WebSocket

View File

@ -60,7 +60,7 @@ const CreateKnowledgeBaseModal = ({
const getAvailableTypes = () => { const getAvailableTypes = () => {
if (isAdmin) { if (isAdmin) {
return [ return [
{ value: 'admin', label: 'Admin 级知识库' }, { value: 'admin', label: '公共知识库' },
{ value: 'leader', label: 'Leader 级知识库' }, { value: 'leader', label: 'Leader 级知识库' },
{ value: 'member', label: 'Member 级知识库' }, { value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }, { value: 'private', label: '私有知识库' },
@ -68,11 +68,15 @@ const CreateKnowledgeBaseModal = ({
]; ];
} else if (isLeader) { } else if (isLeader) {
return [ return [
{ value: 'admin', label: '公共知识库' },
{ value: 'member', label: 'Member 级知识库' }, { value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }, { value: 'private', label: '私有知识库' },
]; ];
} else { } else {
return [{ value: 'private', label: '私有知识库' }]; return [
{ value: 'admin', label: '公共知识库' },
{ value: 'private', label: '私有知识库' },
];
} }
}; };
@ -169,7 +173,7 @@ const CreateKnowledgeBaseModal = ({
</div> </div>
{!isAdmin && !isLeader && ( {!isAdmin && !isLeader && (
<small className='text-muted d-block mt-1'> <small className='text-muted d-block mt-1'>
注意您当前只能创建私有知识库其他类型需要更高权限 您可以创建公共知识库所有人可访问或私有知识库仅自己可访问
</small> </small>
)} )}
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>} {formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}

View File

@ -173,6 +173,17 @@ export default function SettingsTab({ knowledgeBase }) {
return; return;
} }
// adminprivate
if (newType !== 'admin' && newType !== 'private' && currentUser.role === 'member') {
dispatch(
showNotification({
message: '您只能将知识库修改为公共(admin)或私有(private)类型',
type: 'warning',
})
);
return;
}
if (isAdmin && !validateForm()) { if (isAdmin && !validateForm()) {
return; return;
} }

View File

@ -26,7 +26,7 @@ const KnowledgeBaseForm = ({
const getAvailableTypes = () => { const getAvailableTypes = () => {
if (isAdmin) { if (isAdmin) {
return [ return [
{ value: 'admin', label: 'Admin 级知识库' }, { value: 'admin', label: '公共知识库' },
{ value: 'leader', label: 'Leader 级知识库' }, { value: 'leader', label: 'Leader 级知识库' },
{ value: 'member', label: 'Member 级知识库' }, { value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }, { value: 'private', label: '私有知识库' },
@ -34,11 +34,15 @@ const KnowledgeBaseForm = ({
]; ];
} else if (isLeader) { } else if (isLeader) {
return [ return [
{ value: 'admin', label: '公共知识库' },
{ value: 'member', label: 'Member 级知识库' }, { value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }, { value: 'private', label: '私有知识库' },
]; ];
} else { } else {
return [{ value: 'private', label: '私有知识库' }]; return [
{ value: 'admin', label: '公共知识库' },
{ value: 'private', label: '私有知识库' },
];
} }
}; };
@ -106,7 +110,7 @@ const KnowledgeBaseForm = ({
value={type.value} value={type.value}
checked={formData.type === type.value} checked={formData.type === type.value}
onChange={onInputChange} onChange={onInputChange}
disabled={currentUser?.role === 'member'} // disabled={false} //
/> />
<label className='form-check-label' htmlFor={`type${type.value}`}> <label className='form-check-label' htmlFor={`type${type.value}`}>
{type.label} {type.label}
@ -115,7 +119,7 @@ const KnowledgeBaseForm = ({
))} ))}
</div> </div>
{currentUser?.role === 'member' && ( {currentUser?.role === 'member' && (
<small className='text-muted d-block mt-1'>当前无权修改知识库类型</small> <small className='text-muted d-block mt-1'>可以修改知识库类型为公共或私有</small>
)} )}
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>} {formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}
</div> </div>

View File

@ -437,8 +437,8 @@ export default function KnowledgeBase() {
const isAdmin = currentUser?.role === 'admin'; const isAdmin = currentUser?.role === 'admin';
const isLeader = currentUser?.role === 'leader'; const isLeader = currentUser?.role === 'leader';
// //
let defaultType = 'private'; let defaultType = 'admin';
// //
let department = currentUser?.department || ''; let department = currentUser?.department || '';

View File

@ -12,9 +12,6 @@ import './PendingRequests.css'; // 引入外部CSS文件
import SvgIcon from '../../../components/SvgIcon'; import SvgIcon from '../../../components/SvgIcon';
import RequestDetailSlideOver from './RequestDetailSlideOver'; import RequestDetailSlideOver from './RequestDetailSlideOver';
//
const PAGE_SIZE = 5;
export default function PendingRequests() { export default function PendingRequests() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const location = useLocation(); const location = useLocation();
@ -25,14 +22,18 @@ export default function PendingRequests() {
const [selectedRequest, setSelectedRequest] = useState(null); const [selectedRequest, setSelectedRequest] = useState(null);
const [showSlideOver, setShowSlideOver] = useState(false); const [showSlideOver, setShowSlideOver] = useState(false);
// 使 // Redux store
const [pendingRequests, setPendingRequests] = useState([]); const {
const [fetchStatus, setFetchStatus] = useState('idle'); results: pendingRequests,
const [fetchError, setFetchError] = useState(null); status: fetchStatus,
error: fetchError,
page: currentPage,
page_size: pageSize,
total,
} = useSelector((state) => state.permissions.pending);
// //
const [currentPage, setCurrentPage] = useState(1); const totalPages = Math.ceil(total / pageSize) || 1;
const [totalPages, setTotalPages] = useState(1);
// Redux store/ // Redux store/
const { const {
@ -43,24 +44,8 @@ export default function PendingRequests() {
// //
useEffect(() => { useEffect(() => {
const fetchData = async () => { dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize }));
try { }, [dispatch, currentPage, pageSize]);
setFetchStatus('loading');
const result = await dispatch(fetchPermissionsThunk());
if (result.payload) {
setPendingRequests(result.payload);
setTotalPages(Math.ceil(result.payload.length / PAGE_SIZE));
}
setFetchStatus('succeeded');
} catch (error) {
console.error('获取待处理申请列表失败:', error);
setFetchError(error.message || '获取待处理申请列表失败');
setFetchStatus('failed');
}
};
fetchData();
}, [dispatch]);
// //
useEffect(() => { useEffect(() => {
@ -88,17 +73,8 @@ export default function PendingRequests() {
setShowSlideOver(false); setShowSlideOver(false);
setSelectedRequest(null); setSelectedRequest(null);
// //
const updatedRequests = pendingRequests.filter((req) => req.id !== currentRequestId); dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize }));
setPendingRequests(updatedRequests);
//
setTotalPages(Math.ceil(updatedRequests.length / PAGE_SIZE));
//
if (getCurrentPageData().length === 1 && currentPage > 1) {
setCurrentPage(currentPage - 1);
}
// //
dispatch(resetOperationStatus()); dispatch(resetOperationStatus());
@ -112,15 +88,7 @@ export default function PendingRequests() {
// //
dispatch(resetOperationStatus()); dispatch(resetOperationStatus());
} }
}, [ }, [approveRejectStatus, approveRejectError, dispatch, isApproving, currentPage, pageSize]);
approveRejectStatus,
approveRejectError,
dispatch,
isApproving,
currentRequestId,
pendingRequests,
currentPage,
]);
// //
const handleOpenResponseInput = (requestId, approving) => { const handleOpenResponseInput = (requestId, approving) => {
@ -183,16 +151,9 @@ export default function PendingRequests() {
} }
}; };
//
const getCurrentPageData = () => {
const startIndex = (currentPage - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE;
return Array.isArray(pendingRequests) ? pendingRequests.slice(startIndex, endIndex) : [];
};
// //
const handlePageChange = (page) => { const handlePageChange = (page) => {
setCurrentPage(page); dispatch(fetchPermissionsThunk({ page, page_size: pageSize }));
}; };
// //
@ -254,29 +215,27 @@ export default function PendingRequests() {
); );
} }
//
const currentPageData = getCurrentPageData();
// //
return ( return (
<> <>
<div className='d-flex justify-content-between align-items-center mb-3'> <div className='d-flex justify-content-between align-items-center mb-3'>
<h5 className='mb-0'>待处理申请</h5> <h5 className='mb-0'>待处理申请</h5>
<div className='badge bg-danger'>{pendingRequests.length}个待处理</div> <div className='badge bg-danger'>{total}个待处理</div>
</div> </div>
<div className='pending-requests-list'> <div className='pending-requests-list'>
{currentPageData.map((request) => ( {pendingRequests.map((request) => (
<div key={request.id} className='pending-request-item' onClick={() => handleRowClick(request)}> <div key={request.id} className='pending-request-item' onClick={() => handleRowClick(request)}>
<div className='request-header'> <div className='request-header'>
<div className='user-info'> <div className='user-info'>
<h6 className='mb-0'>{request.applicant}</h6> <h6 className='mb-0'>{request.applicant.name || request.applicant.username}</h6>
<small className='text-muted'>{request.applicant.department || '未分配部门'}</small>
</div> </div>
<div className='request-date'>{new Date(request.created_at).toLocaleDateString()}</div> <div className='request-date'>{new Date(request.created_at).toLocaleDateString()}</div>
</div> </div>
<div className='request-content'> <div className='request-content'>
<p className='mb-2'>申请访问{request.knowledge_base}</p> <p className='mb-2'>申请访问{request.knowledge_base.name}</p>
{request.permissions.can_edit ? ( {request.permissions.can_edit ? (
<span <span
@ -297,6 +256,14 @@ export default function PendingRequests() {
</span> </span>
) )
)} )}
{request.expires_at && (
<div className='mt-2'>
<small className='text-muted'>
到期时间: {new Date(request.expires_at).toLocaleDateString()}
</small>
</div>
)}
</div> </div>
<div className='request-actions'> <div className='request-actions'>
<button <button
@ -346,7 +313,7 @@ export default function PendingRequests() {
{/* 回复输入弹窗 */} {/* 回复输入弹窗 */}
{showResponseInput && ( {showResponseInput && (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'> <div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog'> <div className='modal-dialog' style={{ zIndex: 9999 }}>
<div className='modal-content'> <div className='modal-content'>
<div className='modal-header'> <div className='modal-header'>
<h5 className='modal-title'>{isApproving ? '批准' : '拒绝'}申请</h5> <h5 className='modal-title'>{isApproving ? '批准' : '拒绝'}申请</h5>

View File

@ -13,9 +13,13 @@ export default function RequestDetailSlideOver({
}) { }) {
if (!request) return null; if (!request) return null;
// //
const applicant = request.applicant || request.title || '未知用户'; const applicantName = request.applicant?.name || request.applicant?.username || '未知用户';
const applicantInitial = applicant.charAt(0); const applicantDept = request.applicant?.department || '未分配部门';
const applicantInitial = applicantName.charAt(0);
const knowledgeBaseName = request.knowledge_base?.name || '未知知识库';
const knowledgeBaseId = request.knowledge_base?.id || '';
const knowledgeBaseType = request.knowledge_base?.type || '';
return ( return (
<> <>
@ -32,7 +36,8 @@ export default function RequestDetailSlideOver({
<div className='d-flex align-items-center mb-3'> <div className='d-flex align-items-center mb-3'>
<div className='avatar-placeholder me-3 bg-dark'>{applicantInitial}</div> <div className='avatar-placeholder me-3 bg-dark'>{applicantInitial}</div>
<div> <div>
<h5 className='mb-1'>{applicant}</h5> <h5 className='mb-1'>{applicantName}</h5>
<div className='text-muted'>{applicantDept}</div>
</div> </div>
</div> </div>
</div> </div>
@ -40,8 +45,16 @@ export default function RequestDetailSlideOver({
<div className='mb-4'> <div className='mb-4'>
<h6 className='text-muted mb-2'>知识库信息</h6> <h6 className='text-muted mb-2'>知识库信息</h6>
<p className='mb-1'> <p className='mb-1'>
<strong>ID</strong> {request.knowledge_base || request.content || '未知知识库'} <strong>名称</strong> {knowledgeBaseName}
</p> </p>
<p className='mb-1'>
<strong>ID</strong> {knowledgeBaseId}
</p>
{knowledgeBaseType && (
<p className='mb-1'>
<strong>类型</strong> {knowledgeBaseType}
</p>
)}
</div> </div>
<div className='mb-4'> <div className='mb-4'>
@ -49,12 +62,20 @@ export default function RequestDetailSlideOver({
<div className='d-flex flex-wrap gap-2 mb-3'> <div className='d-flex flex-wrap gap-2 mb-3'>
{request.permissions?.can_read && !request.permissions?.can_edit && ( {request.permissions?.can_read && !request.permissions?.can_edit && (
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'> <span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
<SvgIcon className={'eye'} />
只读 只读
</span> </span>
)} )}
{request.permissions?.can_edit && request.permissions?.can_delete && ( {request.permissions?.can_edit && (
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'> <span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
完全访问 <SvgIcon className={'pencil'} />
编辑
</span>
)}
{request.permissions?.can_delete && (
<span className='badge bg-danger-subtle text-danger d-flex align-items-center gap-1'>
<SvgIcon className={'trash'} />
删除
</span> </span>
)} )}
</div> </div>
@ -72,51 +93,84 @@ export default function RequestDetailSlideOver({
</div> </div>
)} )}
{request.status && (
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请状态</h6>
<p className='mb-1'>
{request.status === 'pending' ? (
<span className='badge bg-warning'>待处理</span>
) : request.status === 'approved' ? (
<span className='badge bg-success'>已批准</span>
) : (
<span className='badge bg-danger'>已拒绝</span>
)}
</p>
</div>
)}
{request.approver && (
<div className='mb-4'>
<h6 className='text-muted mb-2'>审批人</h6>
<p className='mb-1'>
{request.approver.name || request.approver.username} ({request.approver.department || '未分配部门'})
</p>
</div>
)}
{request.response_message && (
<div className='mb-4'>
<h6 className='text-muted mb-2'>审批意见</h6>
<div className='p-3 bg-light rounded'>{request.response_message}</div>
</div>
)}
<div className='mb-4'> <div className='mb-4'>
<h6 className='text-muted mb-2'>申请理由</h6> <h6 className='text-muted mb-2'>申请理由</h6>
<div className='p-3 bg-light rounded'> <div className='p-3 bg-light rounded'>
{request.reason || request.content || '无申请理由'} {request.reason || '无申请理由'}
</div> </div>
</div> </div>
</div> </div>
<div className='slide-over-footer'> {request.status === 'pending' && (
<button <div className='slide-over-footer'>
className='btn btn-outline-danger me-2' <button
onClick={() => onReject(request.id)} className='btn btn-outline-danger me-2'
disabled={processingId === request.id && approveRejectStatus === 'loading'} onClick={() => onReject(request.id)}
> disabled={processingId === request.id && approveRejectStatus === 'loading'}
{processingId === request.id && approveRejectStatus === 'loading' && !isApproving ? ( >
<> {processingId === request.id && approveRejectStatus === 'loading' && !isApproving ? (
<span <>
className='spinner-border spinner-border-sm me-1' <span
role='status' className='spinner-border spinner-border-sm me-1'
aria-hidden='true' role='status'
></span> aria-hidden='true'
处理中... ></span>
</> 处理中...
) : ( </>
<>拒绝</> ) : (
)} <>拒绝</>
</button> )}
<button </button>
className='btn btn-outline-success' <button
onClick={() => onApprove(request.id)} className='btn btn-outline-success'
disabled={processingId === request.id && approveRejectStatus === 'loading'} onClick={() => onApprove(request.id)}
> disabled={processingId === request.id && approveRejectStatus === 'loading'}
{processingId === request.id && approveRejectStatus === 'loading' && isApproving ? ( >
<> {processingId === request.id && approveRejectStatus === 'loading' && isApproving ? (
<span <>
className='spinner-border spinner-border-sm me-1' <span
role='status' className='spinner-border spinner-border-sm me-1'
aria-hidden='true' role='status'
></span> aria-hidden='true'
处理中... ></span>
</> 处理中...
) : ( </>
<>批准</> ) : (
)} <>批准</>
</button> )}
</div> </button>
</div>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -1,120 +1,93 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { get } from '../../../services/api'; import { get, post } from '../../../services/api';
//
const mockUserPermissions = [
{
knowledge_base: {
id: '1',
name: '达人直播数据报告',
department: '达人组',
},
permission: {
can_read: true,
can_edit: true,
can_admin: false,
},
last_access_time: '2024-03-10T14:30:00Z',
},
{
knowledge_base: {
id: '2',
name: '人力资源政策文件',
department: '人力资源组',
},
permission: {
can_read: true,
can_edit: false,
can_admin: false,
},
last_access_time: '2024-03-08T09:15:00Z',
},
{
knowledge_base: {
id: '3',
name: '市场分析报告',
department: '市场部',
},
permission: {
can_read: true,
can_edit: false,
can_admin: false,
},
last_access_time: null,
},
{
knowledge_base: {
id: '4',
name: '产品规划文档',
department: '产品部',
},
permission: {
can_read: true,
can_edit: true,
can_admin: true,
},
last_access_time: '2024-03-15T11:20:00Z',
},
];
export default function UserPermissionDetails({ user, onClose, onSave }) { export default function UserPermissionDetails({ user, onClose, onSave }) {
const [updatedPermissions, setUpdatedPermissions] = useState({});
const [userPermissions, setUserPermissions] = useState([]); const [userPermissions, setUserPermissions] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [updatedPermissions, setUpdatedPermissions] = useState({}); const [savingPermissions, setSavingPermissions] = useState(false);
const [successMessage, setSuccessMessage] = useState(null);
//
useEffect(() => { useEffect(() => {
const fetchUserPermissions = async () => { // If we already have the permissions data in the user object, use it
try { if (user.permissions && Array.isArray(user.permissions)) {
setLoading(true); setUserPermissions(user.permissions);
const response = await get(`/users/${user.user.id}/permissions/`); setLoading(false);
} else {
if (response && response.code === 200) { // Otherwise fetch the detailed permissions
// API
const apiPermissions = response.data.permissions || [];
if (apiPermissions.length > 0) {
// 使API
setUserPermissions(apiPermissions);
console.log('使用API返回的用户权限数据');
} else {
// API使
console.log('API返回的用户权限数据为空使用模拟数据');
setUserPermissions(mockUserPermissions);
}
} else {
// API使
console.log('API请求失败使用模拟数据');
setUserPermissions(mockUserPermissions);
setError('获取用户权限详情失败,显示模拟数据');
}
} catch (error) {
console.error('获取用户权限详情失败:', error);
// API使
console.log('API请求出错使用模拟数据作为后备');
setUserPermissions(mockUserPermissions);
setError('获取用户权限详情失败,显示模拟数据');
} finally {
setLoading(false);
}
};
if (user) {
fetchUserPermissions(); fetchUserPermissions();
} }
}, [user]); }, [user]);
// const fetchUserPermissions = async () => {
const handlePermissionChange = (knowledgeBaseId, permissionType) => { setLoading(true);
setUpdatedPermissions({ try {
...updatedPermissions, const response = await get(`/api/users/${user.user_info.id}/permissions`);
[knowledgeBaseId]: permissionType, setUserPermissions(response.permissions || []);
}); setError(null);
} catch (err) {
setError('获取用户权限详情失败: ' + (err.message || '未知错误'));
} finally {
setLoading(false);
}
}; };
// const handlePermissionChange = (knowledgeBaseId, newPermissionType) => {
const handleSave = () => { setUpdatedPermissions((prev) => ({
onSave(user.user.id, updatedPermissions); ...prev,
[knowledgeBaseId]: newPermissionType,
}));
};
const handleSave = async () => {
setSavingPermissions(true);
setError(null);
setSuccessMessage(null);
try {
const permissionUpdates = Object.entries(updatedPermissions).map(
async ([knowledgeBaseId, permissionType]) => {
const permissions = {
can_read: permissionType !== 'none',
can_edit: ['edit', 'admin'].includes(permissionType),
can_delete: permissionType === 'admin',
};
const requestBody = {
user_id: user.user_info.id,
knowledge_base_id: knowledgeBaseId,
permissions: permissions,
// Optional expiration date - can be added if needed
// expires_at: "2025-12-31T23:59:59Z"
};
try {
const response = await post('/permissions/update_permission/', requestBody);
return response;
} catch (err) {
throw new Error(`更新知识库 ${knowledgeBaseId} 权限失败: ${err.message || '未知错误'}`);
}
}
);
await Promise.all(permissionUpdates);
setSuccessMessage('权限更新成功');
// Refresh permissions list
fetchUserPermissions();
// Reset updated permissions
setUpdatedPermissions({});
// Notify parent component if needed
if (onSave) {
onSave(user.user_info.id);
}
} catch (err) {
setError(err.message || '更新权限时发生错误');
} finally {
setSavingPermissions(false);
}
}; };
// //
@ -136,7 +109,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
// //
const getPermissionType = (permission) => { const getPermissionType = (permission) => {
if (!permission) return 'none'; if (!permission) return 'none';
if (permission.can_admin) return 'admin'; if (permission.can_delete) return 'admin';
if (permission.can_edit) return 'edit'; if (permission.can_edit) return 'edit';
if (permission.can_read) return 'read'; if (permission.can_read) return 'read';
return 'none'; return 'none';
@ -147,10 +120,21 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
<div className='modal-dialog modal-lg modal-dialog-scrollable' style={{ zIndex: 9999 }}> <div className='modal-dialog modal-lg modal-dialog-scrollable' style={{ zIndex: 9999 }}>
<div className='modal-content'> <div className='modal-content'>
<div className='modal-header'> <div className='modal-header'>
<h5 className='modal-title'>{user.user.name} 的权限详情</h5> <h5 className='modal-title'>{user.user_info.name} 的权限详情</h5>
<button type='button' className='btn-close' onClick={onClose}></button> <button type='button' className='btn-close' onClick={onClose}></button>
</div> </div>
<div className='modal-body'> <div className='modal-body'>
{successMessage && (
<div className='alert alert-success alert-dismissible fade show' role='alert'>
{successMessage}
<button
type='button'
className='btn-close'
onClick={() => setSuccessMessage(null)}
></button>
</div>
)}
{loading ? ( {loading ? (
<div className='text-center py-4'> <div className='text-center py-4'>
<div className='spinner-border' role='status'> <div className='spinner-border' role='status'>
@ -177,7 +161,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</thead> </thead>
<tbody> <tbody>
{userPermissions.map((item) => { {userPermissions.map((item) => {
const currentPermissionType = getPermissionType(item.permission); const currentPermissionType = getPermissionType(item.permissions);
const updatedPermissionType = const updatedPermissionType =
updatedPermissions[item.knowledge_base.id] || updatedPermissions[item.knowledge_base.id] ||
currentPermissionType; currentPermissionType;
@ -202,8 +186,8 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</span> </span>
</td> </td>
<td> <td>
{item.last_access_time {item.granted_at
? new Date(item.last_access_time).toLocaleString() ? new Date(item.granted_at).toLocaleString()
: '从未访问'} : '从未访问'}
</td> </td>
<td> <td>
@ -243,13 +227,13 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
<th>知识库名称</th> <th>知识库名称</th>
<th>所属部门</th> <th>所属部门</th>
<th>当前权限</th> <th>当前权限</th>
<th>最后访问时间</th> <th>授权时间</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{userPermissions.map((item) => { {userPermissions.map((item) => {
const currentPermissionType = getPermissionType(item.permission); const currentPermissionType = getPermissionType(item.permissions);
const updatedPermissionType = const updatedPermissionType =
updatedPermissions[item.knowledge_base.id] || currentPermissionType; updatedPermissions[item.knowledge_base.id] || currentPermissionType;
@ -273,9 +257,9 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</span> </span>
</td> </td>
<td> <td>
{item.last_access_time {item.granted_at
? new Date(item.last_access_time).toLocaleString() ? new Date(item.granted_at).toLocaleString()
: '从未访问'} : '未记录'}
</td> </td>
<td> <td>
<select <select
@ -310,9 +294,20 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
type='button' type='button'
className='btn btn-dark' className='btn btn-dark'
onClick={handleSave} onClick={handleSave}
disabled={loading || Object.keys(updatedPermissions).length === 0} disabled={loading || savingPermissions || Object.keys(updatedPermissions).length === 0}
> >
保存更改 {savingPermissions ? (
<>
<span
className='spinner-border spinner-border-sm me-2'
role='status'
aria-hidden='true'
></span>
保存中...
</>
) : (
'保存更改'
)}
</button> </button>
</div> </div>
</div> </div>

View File

@ -36,8 +36,8 @@ export default function UserPermissions() {
// //
const totalPages = Math.ceil(total / page_size); const totalPages = Math.ceil(total / page_size);
const handleOpenDetailsModal = (user) => { const handleOpenDetailsModal = (data) => {
setSelectedUser(user); setSelectedUser(data);
setShowDetailsModal(true); setShowDetailsModal(true);
}; };
@ -46,11 +46,11 @@ export default function UserPermissions() {
setShowDetailsModal(false); setShowDetailsModal(false);
}; };
const handleSavePermissions = async (userId, updatedPermissions) => { const handleSavePermissions = async (userId) => {
try { try {
await dispatch(updateUserPermissions({ userId, permissions: updatedPermissions })).unwrap(); // Permission updates are now handled directly in the UserPermissionDetails component
// Just refresh the users list to reflect the updated permissions
handleCloseDetailsModal(); handleCloseDetailsModal();
//
dispatch(fetchAllUserPermissions({ page: currentPage, page_size: pageSize })); dispatch(fetchAllUserPermissions({ page: currentPage, page_size: pageSize }));
} catch (error) { } catch (error) {
console.error('更新权限失败:', error); console.error('更新权限失败:', error);
@ -82,10 +82,10 @@ export default function UserPermissions() {
return users.filter( return users.filter(
(user) => (user) =>
user.user.username.toLowerCase().includes(searchTerm.toLowerCase()) || user.user_info?.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user.name.toLowerCase().includes(searchTerm.toLowerCase()) || user.user_info?.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user.department.toLowerCase().includes(searchTerm.toLowerCase()) || user.user_info?.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user.role.toLowerCase().includes(searchTerm.toLowerCase()) user.user_info?.role.toLowerCase().includes(searchTerm.toLowerCase())
); );
}; };
@ -190,12 +190,13 @@ export default function UserPermissions() {
}; };
return ( return (
<div className='card'> <>
<div className='card-header'> <div className='d-flex justify-content-between align-items-center mb-3'>
<h5 className='mb-0'>用户权限管理</h5> <h5 className='mb-0'>用户权限管理</h5>
</div> </div>
<div className='card-body'>
<div className='mb-3 d-flex justify-content-between align-items-center'> <>
{/* <div className='mb-3 d-flex justify-content-between align-items-center'>
<div className='search-bar' style={{ maxWidth: '300px' }}> <div className='search-bar' style={{ maxWidth: '300px' }}>
<div className='input-group'> <div className='input-group'>
<span className='input-group-text bg-light border-end-0'> <span className='input-group-text bg-light border-end-0'>
@ -210,7 +211,7 @@ export default function UserPermissions() {
/> />
</div> </div>
</div> </div>
</div> </div> */}
{loading === 'loading' ? ( {loading === 'loading' ? (
<div className='text-center my-5'> <div className='text-center my-5'>
@ -241,58 +242,70 @@ export default function UserPermissions() {
<tbody> <tbody>
{filteredUsers.length > 0 ? ( {filteredUsers.length > 0 ? (
filteredUsers.map((userPermission) => ( filteredUsers.map((userPermission) => (
<tr key={userPermission.id}> <tr key={userPermission.user_info?.id}>
<td className='align-middle'>{userPermission.id}</td> <td className='align-middle'>{userPermission.user_info?.id}</td>
<td className='align-middle'>{userPermission.user.username}</td> <td className='align-middle'>
<td className='align-middle'>{userPermission.user.name}</td> {userPermission.user_info?.username || 'N/A'}
<td className='align-middle'>{userPermission.user.department}</td> </td>
<td className='align-middle'>
{userPermission.user_info?.name || 'N/A'}
</td>
<td className='align-middle'>
{userPermission.user_info?.department || 'N/A'}
</td>
<td className='align-middle'> <td className='align-middle'>
<span <span
className={`badge ${ className={`badge ${
userPermission.user.role === 'admin' userPermission.user_info?.role === 'admin'
? 'bg-danger-subtle text-danger' ? 'bg-danger-subtle text-danger'
: userPermission.user.role === 'leader' : userPermission.user_info?.role === 'leader'
? 'bg-warning-subtle text-warning' ? 'bg-warning-subtle text-warning'
: 'bg-info-subtle text-info' : 'bg-info-subtle text-info'
}`} }`}
> >
{userPermission.user.role === 'admin' {userPermission.user_info?.role === 'admin'
? '管理员' ? '管理员'
: userPermission.user.role === 'leader' : userPermission.user_info?.role === 'leader'
? '组长' ? '组长'
: '成员'} : '成员'}
</span> </span>
</td> </td>
<td className='align-middle'> <td className='align-middle'>
{userPermission.user.permissions_count && (
<div className='d-flex flex-wrap gap-1'> <div className='d-flex flex-wrap gap-1'>
{userPermission.user.permissions_count.read > 0 && ( {userPermission.stats?.by_permission?.full_access > 0 && (
<span className='badge <span
bg-success-subtle text-success className='badge
d-flex align-items-center gap-1'> bg-success-subtle text-success
完全访问: {userPermission.user. d-flex align-items-center gap-1'
permissions_count.read} >
完全访问:{' '}
{userPermission.stats?.by_permission?.full_access}
</span> </span>
)} )}
{userPermission.user.permissions_count.edit > 0 && (
<span className='badge {userPermission.stats?.by_permission?.read_only > 0 && (
bg-warning-subtle text-warning <span
d-flex align-items-center gap-1'> className='badge
只读访问: {userPermission.user. bg-warning-subtle text-warning
permissions_count.edit} d-flex align-items-center gap-1'
>
只读访问:{' '}
{userPermission.stats?.by_permission?.read_only}
</span> </span>
)} )}
{userPermission.user.permissions_count.admin > 0 && (
<span className='badge {userPermission.stats?.by_permission?.read_write > 0 && (
bg-dark-subtle d-flex <span
align-items-center gap-1'> className='badge
无访问权限: {userPermission.user. bg-info-subtle text-info
permissions_count.admin} d-flex align-items-center gap-1'
>
读写权限:{' '}
{userPermission.stats?.by_permission?.read_write}
</span> </span>
)} )}
</div> </div>
)} </td>
</td>
<td className='align-middle'> <td className='align-middle'>
<button <button
className='btn btn-sm btn-outline-dark' className='btn btn-sm btn-outline-dark'
@ -305,7 +318,7 @@ export default function UserPermissions() {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan='5' className='text-center py-4'> <td colSpan='7' className='text-center py-4'>
无匹配的用户记录 无匹配的用户记录
</td> </td>
</tr> </tr>
@ -325,7 +338,7 @@ export default function UserPermissions() {
onSave={handleSavePermissions} onSave={handleSavePermissions}
/> />
)} )}
</div> </>
</div> </>
); );
} }

View File

@ -27,7 +27,10 @@ const initialState = {
error: null, error: null,
}, },
pending: { pending: {
items: [], results: [], // 更改为results
total: 0, // 添加total
page: 1, // 添加page
page_size: 10, // 添加page_size
status: 'idle', status: 'idle',
error: null, error: null,
}, },
@ -91,7 +94,10 @@ const permissionsSlice = createSlice({
}) })
.addCase(fetchPermissionsThunk.fulfilled, (state, action) => { .addCase(fetchPermissionsThunk.fulfilled, (state, action) => {
state.pending.status = 'succeeded'; state.pending.status = 'succeeded';
state.pending.items = action.payload; state.pending.results = action.payload.results || [];
state.pending.total = action.payload.total || 0;
state.pending.page = action.payload.page || 1;
state.pending.page_size = action.payload.page_size || 10;
}) })
.addCase(fetchPermissionsThunk.rejected, (state, action) => { .addCase(fetchPermissionsThunk.rejected, (state, action) => {
state.pending.status = 'failed'; state.pending.status = 'failed';

View File

@ -5,17 +5,22 @@ import { showNotification } from '../notification.slice';
// 获取权限申请列表 // 获取权限申请列表
export const fetchPermissionsThunk = createAsyncThunk( export const fetchPermissionsThunk = createAsyncThunk(
'permissions/fetchPermissions', 'permissions/fetchPermissions',
async (_, { rejectWithValue }) => { async (params = {}, { rejectWithValue }) => {
try { try {
const { data, message, code } = await get('/permissions/'); const response = await get('/permissions/', { params });
if (code === 200) { if (response && response.code === 200) {
return data.items || []; return {
results: response.data.results || [],
total: response.data.total || 0,
page: response.data.page || 1,
page_size: response.data.page_size || 10,
};
} }
return rejectWithValue('获取权限申请列表失败'); return rejectWithValue('获取权限申请列表失败');
} catch (error) { } catch (error) {
console.error('获取权限申请列表失败:', error); console.error('获取权限申请列表失败:', error);
return rejectWithValue('获取权限申请列表失败'); return rejectWithValue(error.response?.data?.message || '获取权限申请列表失败');
} }
} }
); );