Compare commits

..

No commits in common. "56c4878065fb2ee36e203ce8fb4911e16340e80b" and "1ba460b4cff346107163b3d1b25b9ccabf3d8241" have entirely different histories.

11 changed files with 295 additions and 336 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: '公共知识库' }, { value: 'admin', label: 'Admin 级知识库' },
{ value: 'leader', label: 'Leader 级知识库' }, { value: 'leader', label: 'Leader 级知识库' },
{ value: 'member', label: 'Member 级知识库' }, { value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }, { value: 'private', label: '私有知识库' },
@ -68,15 +68,11 @@ 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 [ return [{ value: 'private', label: '私有知识库' }];
{ value: 'admin', label: '公共知识库' },
{ value: 'private', label: '私有知识库' },
];
} }
}; };
@ -173,7 +169,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,17 +173,6 @@ 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: '公共知识库' }, { value: 'admin', label: 'Admin 级知识库' },
{ value: 'leader', label: 'Leader 级知识库' }, { value: 'leader', label: 'Leader 级知识库' },
{ value: 'member', label: 'Member 级知识库' }, { value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }, { value: 'private', label: '私有知识库' },
@ -34,15 +34,11 @@ 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 [ return [{ value: 'private', label: '私有知识库' }];
{ value: 'admin', label: '公共知识库' },
{ value: 'private', label: '私有知识库' },
];
} }
}; };
@ -110,7 +106,7 @@ const KnowledgeBaseForm = ({
value={type.value} value={type.value}
checked={formData.type === type.value} checked={formData.type === type.value}
onChange={onInputChange} onChange={onInputChange}
disabled={false} // disabled={currentUser?.role === 'member'} //
/> />
<label className='form-check-label' htmlFor={`type${type.value}`}> <label className='form-check-label' htmlFor={`type${type.value}`}>
{type.label} {type.label}
@ -119,7 +115,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 = 'admin'; let defaultType = 'private';
// //
let department = currentUser?.department || ''; let department = currentUser?.department || '';

View File

@ -12,6 +12,9 @@ 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();
@ -22,18 +25,14 @@ 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 { const [pendingRequests, setPendingRequests] = useState([]);
results: pendingRequests, const [fetchStatus, setFetchStatus] = useState('idle');
status: fetchStatus, const [fetchError, setFetchError] = useState(null);
error: fetchError,
page: currentPage,
page_size: pageSize,
total,
} = useSelector((state) => state.permissions.pending);
// //
const totalPages = Math.ceil(total / pageSize) || 1; const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
// Redux store/ // Redux store/
const { const {
@ -44,8 +43,24 @@ export default function PendingRequests() {
// //
useEffect(() => { useEffect(() => {
dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize })); const fetchData = async () => {
}, [dispatch, currentPage, pageSize]); try {
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(() => {
@ -73,8 +88,17 @@ export default function PendingRequests() {
setShowSlideOver(false); setShowSlideOver(false);
setSelectedRequest(null); setSelectedRequest(null);
// //
dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize })); const updatedRequests = pendingRequests.filter((req) => req.id !== currentRequestId);
setPendingRequests(updatedRequests);
//
setTotalPages(Math.ceil(updatedRequests.length / PAGE_SIZE));
//
if (getCurrentPageData().length === 1 && currentPage > 1) {
setCurrentPage(currentPage - 1);
}
// //
dispatch(resetOperationStatus()); dispatch(resetOperationStatus());
@ -88,7 +112,15 @@ 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) => {
@ -151,9 +183,16 @@ 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) => {
dispatch(fetchPermissionsThunk({ page, page_size: pageSize })); setCurrentPage(page);
}; };
// //
@ -215,27 +254,29 @@ 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'>{total}个待处理</div> <div className='badge bg-danger'>{pendingRequests.length}个待处理</div>
</div> </div>
<div className='pending-requests-list'> <div className='pending-requests-list'>
{pendingRequests.map((request) => ( {currentPageData.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.name || request.applicant.username}</h6> <h6 className='mb-0'>{request.applicant}</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.name}</p> <p className='mb-2'>申请访问{request.knowledge_base}</p>
{request.permissions.can_edit ? ( {request.permissions.can_edit ? (
<span <span
@ -256,14 +297,6 @@ 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
@ -313,7 +346,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' style={{ zIndex: 9999 }}> <div className='modal-dialog'>
<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,13 +13,9 @@ export default function RequestDetailSlideOver({
}) { }) {
if (!request) return null; if (!request) return null;
// //
const applicantName = request.applicant?.name || request.applicant?.username || '未知用户'; const applicant = request.applicant || request.title || '未知用户';
const applicantDept = request.applicant?.department || '未分配部门'; const applicantInitial = applicant.charAt(0);
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 (
<> <>
@ -36,8 +32,7 @@ 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'>{applicantName}</h5> <h5 className='mb-1'>{applicant}</h5>
<div className='text-muted'>{applicantDept}</div>
</div> </div>
</div> </div>
</div> </div>
@ -45,16 +40,8 @@ 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>名称</strong> {knowledgeBaseName} <strong>ID</strong> {request.knowledge_base || request.content || '未知知识库'}
</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'>
@ -62,20 +49,12 @@ 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_edit && request.permissions?.can_delete && (
<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>
@ -93,84 +72,51 @@ 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.reason || request.content || '无申请理由'}
</div> </div>
</div> </div>
</div> </div>
{request.status === 'pending' && ( <div className='slide-over-footer'>
<div className='slide-over-footer'> <button
<button className='btn btn-outline-danger me-2'
className='btn btn-outline-danger me-2' onClick={() => onReject(request.id)}
onClick={() => onReject(request.id)} disabled={processingId === request.id && approveRejectStatus === 'loading'}
disabled={processingId === request.id && approveRejectStatus === 'loading'} >
> {processingId === request.id && approveRejectStatus === 'loading' && !isApproving ? (
{processingId === request.id && approveRejectStatus === 'loading' && !isApproving ? ( <>
<> <span
<span className='spinner-border spinner-border-sm me-1'
className='spinner-border spinner-border-sm me-1' role='status'
role='status' aria-hidden='true'
aria-hidden='true' ></span>
></span> 处理中...
处理中... </>
</> ) : (
) : ( <>拒绝</>
<>拒绝</> )}
)} </button>
</button> <button
<button className='btn btn-outline-success'
className='btn btn-outline-success' onClick={() => onApprove(request.id)}
onClick={() => onApprove(request.id)} disabled={processingId === request.id && approveRejectStatus === 'loading'}
disabled={processingId === request.id && approveRejectStatus === 'loading'} >
> {processingId === request.id && approveRejectStatus === 'loading' && isApproving ? (
{processingId === request.id && approveRejectStatus === 'loading' && isApproving ? ( <>
<> <span
<span className='spinner-border spinner-border-sm me-1'
className='spinner-border spinner-border-sm me-1' role='status'
role='status' aria-hidden='true'
aria-hidden='true' ></span>
></span> 处理中...
处理中... </>
</> ) : (
) : ( <>批准</>
<>批准</> )}
)} </button>
</button> </div>
</div>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -1,75 +1,120 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { get, post } from '../../../services/api'; import { get } 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 [savingPermissions, setSavingPermissions] = useState(false); const [updatedPermissions, setUpdatedPermissions] = useState({});
const [successMessage, setSuccessMessage] = useState(null);
//
useEffect(() => { useEffect(() => {
// If we already have the permissions data in the user object, use it const fetchUserPermissions = async () => {
if (user.permissions && Array.isArray(user.permissions)) { try {
setUserPermissions(user.permissions); setLoading(true);
setLoading(false); const response = await get(`/users/${user.user.id}/permissions/`);
if (response && response.code === 200) {
// 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();
} }
}, [user]); }, [user]);
const handlePermissionChange = (knowledgeBaseId, newPermissionType) => { //
setUpdatedPermissions((prev) => ({ const handlePermissionChange = (knowledgeBaseId, permissionType) => {
...prev, setUpdatedPermissions({
[knowledgeBaseId]: newPermissionType, ...updatedPermissions,
})); [knowledgeBaseId]: permissionType,
});
}; };
const handleSave = async () => { //
setSavingPermissions(true); const handleSave = () => {
setError(null); onSave(user.user.id, updatedPermissions);
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('权限更新成功');
// Reset updated permissions
setUpdatedPermissions({});
// Notify parent component if needed
if (onSave) {
onSave(user.user_info.id);
}
} catch (err) {
setError(err.message || '更新权限时发生错误');
} finally {
setSavingPermissions(false);
}
}; };
// //
@ -91,7 +136,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_delete) return 'admin'; if (permission.can_admin) 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';
@ -102,21 +147,10 @@ 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_info.name} 的权限详情</h5> <h5 className='modal-title'>{user.user.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'>
@ -143,7 +177,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</thead> </thead>
<tbody> <tbody>
{userPermissions.map((item) => { {userPermissions.map((item) => {
const currentPermissionType = getPermissionType(item.permissions); const currentPermissionType = getPermissionType(item.permission);
const updatedPermissionType = const updatedPermissionType =
updatedPermissions[item.knowledge_base.id] || updatedPermissions[item.knowledge_base.id] ||
currentPermissionType; currentPermissionType;
@ -168,8 +202,8 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</span> </span>
</td> </td>
<td> <td>
{item.granted_at {item.last_access_time
? new Date(item.granted_at).toLocaleString() ? new Date(item.last_access_time).toLocaleString()
: '从未访问'} : '从未访问'}
</td> </td>
<td> <td>
@ -209,13 +243,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.permissions); const currentPermissionType = getPermissionType(item.permission);
const updatedPermissionType = const updatedPermissionType =
updatedPermissions[item.knowledge_base.id] || currentPermissionType; updatedPermissions[item.knowledge_base.id] || currentPermissionType;
@ -239,9 +273,9 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</span> </span>
</td> </td>
<td> <td>
{item.granted_at {item.last_access_time
? new Date(item.granted_at).toLocaleString() ? new Date(item.last_access_time).toLocaleString()
: '未记录'} : '从未访问'}
</td> </td>
<td> <td>
<select <select
@ -276,20 +310,9 @@ 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 || savingPermissions || Object.keys(updatedPermissions).length === 0} disabled={loading || 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 = (data) => { const handleOpenDetailsModal = (user) => {
setSelectedUser(data); setSelectedUser(user);
setShowDetailsModal(true); setShowDetailsModal(true);
}; };
@ -46,11 +46,11 @@ export default function UserPermissions() {
setShowDetailsModal(false); setShowDetailsModal(false);
}; };
const handleSavePermissions = async (userId) => { const handleSavePermissions = async (userId, updatedPermissions) => {
try { try {
// Permission updates are now handled directly in the UserPermissionDetails component await dispatch(updateUserPermissions({ userId, permissions: updatedPermissions })).unwrap();
// 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_info?.username.toLowerCase().includes(searchTerm.toLowerCase()) || user.user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user_info?.name.toLowerCase().includes(searchTerm.toLowerCase()) || user.user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user_info?.department.toLowerCase().includes(searchTerm.toLowerCase()) || user.user.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user_info?.role.toLowerCase().includes(searchTerm.toLowerCase()) user.user.role.toLowerCase().includes(searchTerm.toLowerCase())
); );
}; };
@ -190,13 +190,12 @@ export default function UserPermissions() {
}; };
return ( return (
<> <div className='card'>
<div className='d-flex justify-content-between align-items-center mb-3'> <div className='card-header'>
<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'>
@ -211,7 +210,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'>
@ -242,70 +241,58 @@ export default function UserPermissions() {
<tbody> <tbody>
{filteredUsers.length > 0 ? ( {filteredUsers.length > 0 ? (
filteredUsers.map((userPermission) => ( filteredUsers.map((userPermission) => (
<tr key={userPermission.user_info?.id}> <tr key={userPermission.id}>
<td className='align-middle'>{userPermission.user_info?.id}</td> <td className='align-middle'>{userPermission.id}</td>
<td className='align-middle'> <td className='align-middle'>{userPermission.user.username}</td>
{userPermission.user_info?.username || 'N/A'} <td className='align-middle'>{userPermission.user.name}</td>
</td> <td className='align-middle'>{userPermission.user.department}</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_info?.role === 'admin' userPermission.user.role === 'admin'
? 'bg-danger-subtle text-danger' ? 'bg-danger-subtle text-danger'
: userPermission.user_info?.role === 'leader' : userPermission.user.role === 'leader'
? 'bg-warning-subtle text-warning' ? 'bg-warning-subtle text-warning'
: 'bg-info-subtle text-info' : 'bg-info-subtle text-info'
}`} }`}
> >
{userPermission.user_info?.role === 'admin' {userPermission.user.role === 'admin'
? '管理员' ? '管理员'
: userPermission.user_info?.role === 'leader' : userPermission.user.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.stats?.by_permission?.full_access > 0 && ( {userPermission.user.permissions_count.read > 0 && (
<span <span className='badge
className='badge bg-success-subtle text-success
bg-success-subtle text-success d-flex align-items-center gap-1'>
d-flex align-items-center gap-1' 完全访问: {userPermission.user.
> permissions_count.read}
完全访问:{' '}
{userPermission.stats?.by_permission?.full_access}
</span> </span>
)} )}
{userPermission.user.permissions_count.edit > 0 && (
{userPermission.stats?.by_permission?.read_only > 0 && ( <span className='badge
<span bg-warning-subtle text-warning
className='badge d-flex align-items-center gap-1'>
bg-warning-subtle text-warning 只读访问: {userPermission.user.
d-flex align-items-center gap-1' permissions_count.edit}
>
只读访问:{' '}
{userPermission.stats?.by_permission?.read_only}
</span> </span>
)} )}
{userPermission.user.permissions_count.admin > 0 && (
{userPermission.stats?.by_permission?.read_write > 0 && ( <span className='badge
<span bg-dark-subtle d-flex
className='badge align-items-center gap-1'>
bg-info-subtle text-info 无访问权限: {userPermission.user.
d-flex align-items-center gap-1' permissions_count.admin}
>
读写权限:{' '}
{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'
@ -318,7 +305,7 @@ export default function UserPermissions() {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan='7' className='text-center py-4'> <td colSpan='5' className='text-center py-4'>
无匹配的用户记录 无匹配的用户记录
</td> </td>
</tr> </tr>
@ -338,7 +325,7 @@ export default function UserPermissions() {
onSave={handleSavePermissions} onSave={handleSavePermissions}
/> />
)} )}
</> </div>
</> </div>
); );
} }

View File

@ -27,10 +27,7 @@ const initialState = {
error: null, error: null,
}, },
pending: { pending: {
results: [], // 更改为results items: [],
total: 0, // 添加total
page: 1, // 添加page
page_size: 10, // 添加page_size
status: 'idle', status: 'idle',
error: null, error: null,
}, },
@ -94,10 +91,7 @@ const permissionsSlice = createSlice({
}) })
.addCase(fetchPermissionsThunk.fulfilled, (state, action) => { .addCase(fetchPermissionsThunk.fulfilled, (state, action) => {
state.pending.status = 'succeeded'; state.pending.status = 'succeeded';
state.pending.results = action.payload.results || []; state.pending.items = action.payload;
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,22 +5,17 @@ import { showNotification } from '../notification.slice';
// 获取权限申请列表 // 获取权限申请列表
export const fetchPermissionsThunk = createAsyncThunk( export const fetchPermissionsThunk = createAsyncThunk(
'permissions/fetchPermissions', 'permissions/fetchPermissions',
async (params = {}, { rejectWithValue }) => { async (_, { rejectWithValue }) => {
try { try {
const response = await get('/permissions/', { params }); const { data, message, code } = await get('/permissions/');
if (response && response.code === 200) { if (code === 200) {
return { return data.items || [];
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(error.response?.data?.message || '获取权限申请列表失败'); return rejectWithValue('获取权限申请列表失败');
} }
} }
); );