mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 12:01:53 +08:00
[dev]add permission page
This commit is contained in:
parent
c9236cfff4
commit
6cf31165f9
@ -8,6 +8,7 @@ export default function HeaderWithNav() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
console.log('user', user);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
@ -22,6 +23,9 @@ export default function HeaderWithNav() {
|
|||||||
return location.pathname.startsWith(path);
|
return location.pathname.startsWith(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 检查用户是否有管理权限(leader 或 admin)
|
||||||
|
const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className=' navbar navbar-expand-lg p-0'>
|
<header className=' navbar navbar-expand-lg p-0'>
|
||||||
<nav className='navbar navbar-expand-lg border-bottom p-3 w-100'>
|
<nav className='navbar navbar-expand-lg border-bottom p-3 w-100'>
|
||||||
@ -44,7 +48,9 @@ export default function HeaderWithNav() {
|
|||||||
<ul className='navbar-nav me-auto mb-lg-0'>
|
<ul className='navbar-nav me-auto mb-lg-0'>
|
||||||
<li className='nav-item'>
|
<li className='nav-item'>
|
||||||
<Link
|
<Link
|
||||||
className={`nav-link ${isActive('/') && !isActive('/chat') ? 'active' : ''}`}
|
className={`nav-link ${
|
||||||
|
isActive('/') && !isActive('/chat') && !isActive('/permissions') ? 'active' : ''
|
||||||
|
}`}
|
||||||
aria-current='page'
|
aria-current='page'
|
||||||
to='/'
|
to='/'
|
||||||
>
|
>
|
||||||
@ -56,6 +62,16 @@ export default function HeaderWithNav() {
|
|||||||
Chat
|
Chat
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
{hasManagePermission && (
|
||||||
|
<li className='nav-item'>
|
||||||
|
<Link
|
||||||
|
className={`nav-link ${isActive('/permissions') ? 'active' : ''}`}
|
||||||
|
to='/permissions'
|
||||||
|
>
|
||||||
|
权限管理
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{!!user ? (
|
{!!user ? (
|
||||||
<div className='flex-shrink-0 dropdown'>
|
<div className='flex-shrink-0 dropdown'>
|
||||||
|
44
src/pages/Permissions/PermissionsPage.jsx
Normal file
44
src/pages/Permissions/PermissionsPage.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PendingRequests from './components/PendingRequests';
|
||||||
|
import UserPermissions from './components/UserPermissions';
|
||||||
|
|
||||||
|
export default function PermissionsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
const [activeTab, setActiveTab] = useState('pending');
|
||||||
|
|
||||||
|
// 检查用户是否有管理权限(leader 或 admin)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user || (user.role !== 'leader' && user.role !== 'admin')) {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, [user, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='permissions-container container py-4'>
|
||||||
|
|
||||||
|
<ul className='nav nav-tabs mb-4'>
|
||||||
|
<li className='nav-item'>
|
||||||
|
<button
|
||||||
|
className={`nav-link ${activeTab === 'pending' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('pending')}
|
||||||
|
>
|
||||||
|
待处理申请
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className='nav-item'>
|
||||||
|
<button
|
||||||
|
className={`nav-link ${activeTab === 'users' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('users')}
|
||||||
|
>
|
||||||
|
用户权限管理
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className='tab-content'>{activeTab === 'pending' ? <PendingRequests /> : <UserPermissions />}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
37
src/pages/Permissions/components/PendingRequests.css
Normal file
37
src/pages/Permissions/components/PendingRequests.css
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
.permission-requests {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-badge {
|
||||||
|
padding: 0.35em 0.65em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-info {
|
||||||
|
background-color: #0dcaf0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-success {
|
||||||
|
background-color: #198754 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-danger {
|
||||||
|
background-color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-secondary {
|
||||||
|
background-color: #6c757d !important;
|
||||||
|
}
|
292
src/pages/Permissions/components/PendingRequests.jsx
Normal file
292
src/pages/Permissions/components/PendingRequests.jsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { showNotification } from '../../../store/notification.slice';
|
||||||
|
import {
|
||||||
|
fetchPermissionsThunk,
|
||||||
|
approvePermissionThunk,
|
||||||
|
rejectPermissionThunk,
|
||||||
|
} from '../../../store/permissions/permissions.thunks';
|
||||||
|
import { resetApproveRejectStatus } from '../../../store/permissions/permissions.slice';
|
||||||
|
import './PendingRequests.css'; // 引入外部CSS文件
|
||||||
|
|
||||||
|
export default function PendingRequests() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [responseMessage, setResponseMessage] = useState('');
|
||||||
|
const [showResponseInput, setShowResponseInput] = useState(false);
|
||||||
|
const [currentRequestId, setCurrentRequestId] = useState(null);
|
||||||
|
const [isApproving, setIsApproving] = useState(false);
|
||||||
|
|
||||||
|
// 从Redux store获取权限申请列表和状态
|
||||||
|
const {
|
||||||
|
items: pendingRequests,
|
||||||
|
status: fetchStatus,
|
||||||
|
error: fetchError,
|
||||||
|
} = useSelector((state) => state.permissions.permissions);
|
||||||
|
|
||||||
|
const {
|
||||||
|
status: approveRejectStatus,
|
||||||
|
error: approveRejectError,
|
||||||
|
currentId: processingId,
|
||||||
|
} = useSelector((state) => state.permissions.approveReject);
|
||||||
|
|
||||||
|
// 获取待处理申请列表
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchPermissionsThunk());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// 监听批准/拒绝操作的状态变化
|
||||||
|
useEffect(() => {
|
||||||
|
if (approveRejectStatus === 'succeeded') {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: isApproving ? '已批准申请' : '已拒绝申请',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setShowResponseInput(false);
|
||||||
|
setCurrentRequestId(null);
|
||||||
|
setResponseMessage('');
|
||||||
|
// 重置状态
|
||||||
|
dispatch(resetApproveRejectStatus());
|
||||||
|
} else if (approveRejectStatus === 'failed') {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: approveRejectError || '处理申请失败',
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// 重置状态
|
||||||
|
dispatch(resetApproveRejectStatus());
|
||||||
|
}
|
||||||
|
}, [approveRejectStatus, approveRejectError, dispatch, isApproving]);
|
||||||
|
|
||||||
|
// 打开回复输入框
|
||||||
|
const handleOpenResponseInput = (requestId, approving) => {
|
||||||
|
setCurrentRequestId(requestId);
|
||||||
|
setIsApproving(approving);
|
||||||
|
setShowResponseInput(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭回复输入框
|
||||||
|
const handleCloseResponseInput = () => {
|
||||||
|
setShowResponseInput(false);
|
||||||
|
setCurrentRequestId(null);
|
||||||
|
setResponseMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理申请(批准或拒绝)
|
||||||
|
const handleProcessRequest = () => {
|
||||||
|
if (!currentRequestId) return;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
id: currentRequestId,
|
||||||
|
responseMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isApproving) {
|
||||||
|
dispatch(approvePermissionThunk(params));
|
||||||
|
} else {
|
||||||
|
dispatch(rejectPermissionThunk(params));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取权限类型文本和样式
|
||||||
|
const getPermissionTypeText = (permissions) => {
|
||||||
|
if (permissions.can_read && !permissions.can_edit) {
|
||||||
|
return '只读访问';
|
||||||
|
} else if (permissions.can_edit) {
|
||||||
|
return '完全访问';
|
||||||
|
} else {
|
||||||
|
return '未知权限';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染加载状态
|
||||||
|
if (fetchStatus === 'loading') {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染错误状态
|
||||||
|
if (fetchStatus === 'failed') {
|
||||||
|
return (
|
||||||
|
<div className='alert alert-danger' role='alert'>
|
||||||
|
{fetchError || '获取待处理申请失败'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染空状态
|
||||||
|
if (pendingRequests.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='alert alert-info' role='alert'>
|
||||||
|
暂无待处理的权限申请
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染申请列表
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='d-flex justify-content-between align-items-center mb-4'>
|
||||||
|
{/* <h5 className='mb-0'>待处理申请</h5> */}
|
||||||
|
<div className='bg-danger rounded-pill text-white py-1 px-2'>{pendingRequests.length}个待处理</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='permission-requests'>
|
||||||
|
{pendingRequests.map((request) => (
|
||||||
|
<div key={request.id} className='permission-card card mb-3 border-0 shadow-sm bg-light text-dark'>
|
||||||
|
<div className='card-body p-4 d-flex flex-column gap-3'>
|
||||||
|
<div className='d-flex justify-content-between'>
|
||||||
|
<div>
|
||||||
|
<h5 className='mb-1'>{request.applicant.name || request.applicant}</h5>
|
||||||
|
<p className='text-dark-50 mb-0'>{request.applicant.department || '无归属部门'}</p>
|
||||||
|
</div>
|
||||||
|
<div className='text-end'>
|
||||||
|
<div className='text-dark-50 mb-0'>
|
||||||
|
{new Date(request.created_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='d-flex flex-column gap-3'>
|
||||||
|
<div>申请访问:{request.knowledge_base.name || request.knowledge_base}</div>
|
||||||
|
<div className='d-flex flex-wrap gap-2'>
|
||||||
|
{request.permissions.can_read && (
|
||||||
|
<span className='badge bg-info custom-badge'>只读</span>
|
||||||
|
)}
|
||||||
|
{request.permissions.can_edit && (
|
||||||
|
<span className='badge bg-success custom-badge'>编辑</span>
|
||||||
|
)}
|
||||||
|
{request.permissions.can_delete && (
|
||||||
|
<span className='badge bg-danger custom-badge'>删除</span>
|
||||||
|
)}
|
||||||
|
{request.expires_at && (
|
||||||
|
<span className='badge bg-secondary custom-badge'>
|
||||||
|
至 {new Date(request.expires_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='text-dark-50'>
|
||||||
|
<strong>申请理由:</strong> {request.reason}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='d-flex justify-content-end'>
|
||||||
|
<button
|
||||||
|
className='btn btn-outline-danger me-2'
|
||||||
|
onClick={() => handleOpenResponseInput(request.id, false)}
|
||||||
|
disabled={processingId === request.id && approveRejectStatus === 'loading'}
|
||||||
|
>
|
||||||
|
{processingId === request.id &&
|
||||||
|
approveRejectStatus === 'loading' &&
|
||||||
|
!isApproving ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-1'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
处理中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>拒绝</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='btn btn-success'
|
||||||
|
onClick={() => handleOpenResponseInput(request.id, true)}
|
||||||
|
disabled={processingId === request.id && approveRejectStatus === 'loading'}
|
||||||
|
>
|
||||||
|
{processingId === request.id && approveRejectStatus === 'loading' && isApproving ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-1'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
处理中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>批准</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 回复输入弹窗 */}
|
||||||
|
{showResponseInput && (
|
||||||
|
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
|
||||||
|
<div className='modal-dialog'>
|
||||||
|
<div className='modal-content'>
|
||||||
|
<div className='modal-header'>
|
||||||
|
<h5 className='modal-title'>{isApproving ? '批准' : '拒绝'}申请</h5>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn-close'
|
||||||
|
onClick={handleCloseResponseInput}
|
||||||
|
disabled={approveRejectStatus === 'loading'}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div className='modal-body'>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='responseMessage' className='form-label'>
|
||||||
|
审批意见
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className='form-control'
|
||||||
|
id='responseMessage'
|
||||||
|
rows='3'
|
||||||
|
value={responseMessage}
|
||||||
|
onChange={(e) => setResponseMessage(e.target.value)}
|
||||||
|
placeholder={isApproving ? '请输入批准意见(可选)' : '请输入拒绝理由(可选)'}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='modal-footer'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-secondary'
|
||||||
|
onClick={handleCloseResponseInput}
|
||||||
|
disabled={approveRejectStatus === 'loading'}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={`btn ${isApproving ? 'btn-success' : 'btn-danger'}`}
|
||||||
|
onClick={handleProcessRequest}
|
||||||
|
disabled={approveRejectStatus === 'loading'}
|
||||||
|
>
|
||||||
|
{approveRejectStatus === 'loading' ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-1'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
处理中...
|
||||||
|
</>
|
||||||
|
) : isApproving ? (
|
||||||
|
'确认批准'
|
||||||
|
) : (
|
||||||
|
'确认拒绝'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='modal-backdrop fade show'></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
182
src/pages/Permissions/components/UserPermissionDetails.jsx
Normal file
182
src/pages/Permissions/components/UserPermissionDetails.jsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { get } from '../../../services/api';
|
||||||
|
|
||||||
|
export default function UserPermissionDetails({ user, onClose, onSave }) {
|
||||||
|
const [userPermissions, setUserPermissions] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [updatedPermissions, setUpdatedPermissions] = useState({});
|
||||||
|
|
||||||
|
// 获取用户权限详情
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserPermissions = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await get(`/users/${user.id}/permissions/`);
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
setUserPermissions(response.data.permissions || []);
|
||||||
|
} else {
|
||||||
|
setError('获取用户权限详情失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户权限详情失败:', error);
|
||||||
|
setError('获取用户权限详情失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
fetchUserPermissions();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// 处理权限变更
|
||||||
|
const handlePermissionChange = (knowledgeBaseId, permissionType) => {
|
||||||
|
setUpdatedPermissions({
|
||||||
|
...updatedPermissions,
|
||||||
|
[knowledgeBaseId]: permissionType,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理保存
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(user.id, updatedPermissions);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取权限类型的显示文本
|
||||||
|
const getPermissionTypeText = (permissionType) => {
|
||||||
|
switch (permissionType) {
|
||||||
|
case 'none':
|
||||||
|
return '无权限';
|
||||||
|
case 'read':
|
||||||
|
return '只读访问';
|
||||||
|
case 'edit':
|
||||||
|
return '编辑权限';
|
||||||
|
case 'admin':
|
||||||
|
return '管理权限';
|
||||||
|
default:
|
||||||
|
return '未知权限';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取权限类型的值
|
||||||
|
const getPermissionType = (permission) => {
|
||||||
|
if (!permission) return 'none';
|
||||||
|
if (permission.can_admin) return 'admin';
|
||||||
|
if (permission.can_edit) return 'edit';
|
||||||
|
if (permission.can_read) return 'read';
|
||||||
|
return 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
|
||||||
|
<div className='modal-dialog modal-lg modal-dialog-scrollable'>
|
||||||
|
<div className='modal-content'>
|
||||||
|
<div className='modal-header'>
|
||||||
|
<h5 className='modal-title'>{user.name} 的权限详情</h5>
|
||||||
|
<button type='button' className='btn-close' onClick={onClose}></button>
|
||||||
|
</div>
|
||||||
|
<div className='modal-body'>
|
||||||
|
{loading ? (
|
||||||
|
<div className='text-center py-4'>
|
||||||
|
<div className='spinner-border' role='status'>
|
||||||
|
<span className='visually-hidden'>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<p className='mt-3'>加载权限详情...</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className='alert alert-danger' role='alert'>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : userPermissions.length === 0 ? (
|
||||||
|
<div className='alert alert-info' role='alert'>
|
||||||
|
该用户暂无任何知识库权限
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='table-responsive'>
|
||||||
|
<table className='table table-hover'>
|
||||||
|
<thead className='table-light'>
|
||||||
|
<tr>
|
||||||
|
<th>知识库名称</th>
|
||||||
|
<th>所属部门</th>
|
||||||
|
<th>当前权限</th>
|
||||||
|
<th>最后访问时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{userPermissions.map((item) => {
|
||||||
|
const currentPermissionType = getPermissionType(item.permission);
|
||||||
|
const updatedPermissionType =
|
||||||
|
updatedPermissions[item.knowledge_base.id] || currentPermissionType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={item.knowledge_base.id}>
|
||||||
|
<td>{item.knowledge_base.name}</td>
|
||||||
|
<td>{item.knowledge_base.department || '未指定'}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={`badge ${
|
||||||
|
currentPermissionType === 'admin'
|
||||||
|
? 'bg-danger'
|
||||||
|
: currentPermissionType === 'edit'
|
||||||
|
? 'bg-success'
|
||||||
|
: currentPermissionType === 'read'
|
||||||
|
? 'bg-info'
|
||||||
|
: 'bg-secondary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getPermissionTypeText(currentPermissionType)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{item.last_access_time
|
||||||
|
? new Date(item.last_access_time).toLocaleString()
|
||||||
|
: '从未访问'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
className='form-select form-select-sm'
|
||||||
|
value={updatedPermissionType}
|
||||||
|
onChange={(e) =>
|
||||||
|
handlePermissionChange(
|
||||||
|
item.knowledge_base.id,
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value='none'>无权限</option>
|
||||||
|
<option value='read'>只读访问</option>
|
||||||
|
<option value='edit'>编辑权限</option>
|
||||||
|
<option value='admin'>管理权限</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='modal-footer'>
|
||||||
|
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-primary'
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading || Object.keys(updatedPermissions).length === 0}
|
||||||
|
>
|
||||||
|
保存更改
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='modal-backdrop fade show'></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
206
src/pages/Permissions/components/UserPermissions.jsx
Normal file
206
src/pages/Permissions/components/UserPermissions.jsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await get('/users/permissions/');
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
setUsers(response.data.users || []);
|
||||||
|
} else {
|
||||||
|
setError('获取用户列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error);
|
||||||
|
setError('获取用户列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 打开用户权限详情弹窗
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染加载状态
|
||||||
|
if (loading) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染错误状态
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className='alert alert-danger' role='alert'>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染空状态
|
||||||
|
if (users.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='alert alert-info' role='alert'>
|
||||||
|
暂无用户数据
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染用户列表
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='card'>
|
||||||
|
<div className='card-header bg-white'>
|
||||||
|
<h5 className='mb-0'>用户权限管理</h5>
|
||||||
|
</div>
|
||||||
|
<div className='card-body p-0'>
|
||||||
|
<div className='table-responsive'>
|
||||||
|
<table className='table table-hover mb-0'>
|
||||||
|
<thead className='table-light'>
|
||||||
|
<tr>
|
||||||
|
<th>用户名</th>
|
||||||
|
<th>姓名</th>
|
||||||
|
<th>部门</th>
|
||||||
|
<th>职位</th>
|
||||||
|
<th>权限</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td>{user.username}</td>
|
||||||
|
<td>{user.name}</td>
|
||||||
|
<td>{user.department}</td>
|
||||||
|
<td>{user.position}</td>
|
||||||
|
<td>
|
||||||
|
{user.permissions_count && (
|
||||||
|
<div className='d-flex gap-2'>
|
||||||
|
{user.permissions_count.read > 0 && (
|
||||||
|
<span className='badge bg-info'>
|
||||||
|
只读: {user.permissions_count.read}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{user.permissions_count.edit > 0 && (
|
||||||
|
<span className='badge bg-success'>
|
||||||
|
编辑: {user.permissions_count.edit}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{user.permissions_count.admin > 0 && (
|
||||||
|
<span className='badge bg-danger'>
|
||||||
|
管理: {user.permissions_count.admin}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-primary'
|
||||||
|
onClick={() => handleOpenDetailsModal(user)}
|
||||||
|
>
|
||||||
|
权限详情
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用户权限详情弹窗 */}
|
||||||
|
{showDetailsModal && selectedUser && (
|
||||||
|
<UserPermissionDetails
|
||||||
|
user={selectedUser}
|
||||||
|
onClose={handleCloseDetailsModal}
|
||||||
|
onSave={handleSavePermissions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import Mainlayout from '../layouts/Mainlayout';
|
|||||||
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
|
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
|
||||||
import KnowledgeBaseDetail from '../pages/KnowledgeBase/Detail/KnowledgeBaseDetail';
|
import KnowledgeBaseDetail from '../pages/KnowledgeBase/Detail/KnowledgeBaseDetail';
|
||||||
import Chat from '../pages/Chat/Chat';
|
import Chat from '../pages/Chat/Chat';
|
||||||
|
import PermissionsPage from '../pages/Permissions/PermissionsPage';
|
||||||
import Loading from '../components/Loading';
|
import Loading from '../components/Loading';
|
||||||
import Login from '../pages/Auth/Login';
|
import Login from '../pages/Auth/Login';
|
||||||
import Signup from '../pages/Auth/Signup';
|
import Signup from '../pages/Auth/Signup';
|
||||||
@ -13,6 +14,9 @@ import { useSelector } from 'react-redux';
|
|||||||
function AppRouter() {
|
function AppRouter() {
|
||||||
const { user } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
|
// 检查用户是否有管理权限(leader 或 admin)
|
||||||
|
const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@ -65,6 +69,15 @@ function AppRouter() {
|
|||||||
</Mainlayout>
|
</Mainlayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* 权限管理页面路由 - 仅对 leader 或 admin 角色可见 */}
|
||||||
|
<Route
|
||||||
|
path='/permissions'
|
||||||
|
element={
|
||||||
|
<Mainlayout>
|
||||||
|
<PermissionsPage />
|
||||||
|
</Mainlayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path='/login' element={<Login />} />
|
<Route path='/login' element={<Login />} />
|
||||||
<Route path='/signup' element={<Signup />} />
|
<Route path='/signup' element={<Signup />} />
|
||||||
|
55
src/services/permissionService.js
Normal file
55
src/services/permissionService.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { post } from './api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算权限过期时间
|
||||||
|
* @param {string} duration - 权限持续时间,如 '一周', '一个月', '三个月', '六个月', '永久'
|
||||||
|
* @returns {string} - ISO 格式的日期字符串
|
||||||
|
*/
|
||||||
|
export const calculateExpiresAt = (duration) => {
|
||||||
|
const now = new Date();
|
||||||
|
switch (duration) {
|
||||||
|
case '一周':
|
||||||
|
now.setDate(now.getDate() + 7);
|
||||||
|
break;
|
||||||
|
case '一个月':
|
||||||
|
now.setMonth(now.getMonth() + 1);
|
||||||
|
break;
|
||||||
|
case '三个月':
|
||||||
|
now.setMonth(now.getMonth() + 3);
|
||||||
|
break;
|
||||||
|
case '六个月':
|
||||||
|
now.setMonth(now.getMonth() + 6);
|
||||||
|
break;
|
||||||
|
case '永久':
|
||||||
|
// 设置为较远的未来日期
|
||||||
|
now.setFullYear(now.getFullYear() + 10);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
now.setDate(now.getDate() + 7);
|
||||||
|
}
|
||||||
|
return now.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请知识库访问权限
|
||||||
|
* @param {Object} requestData - 请求数据
|
||||||
|
* @param {string} requestData.id - 知识库ID
|
||||||
|
* @param {string} requestData.accessType - 访问类型,如 '只读访问', '编辑权限'
|
||||||
|
* @param {string} requestData.duration - 访问时长,如 '一周', '一个月'
|
||||||
|
* @param {string} requestData.reason - 申请原因
|
||||||
|
* @returns {Promise} - API 请求的 Promise
|
||||||
|
*/
|
||||||
|
export const requestKnowledgeBaseAccess = async (requestData) => {
|
||||||
|
const apiRequestData = {
|
||||||
|
knowledge_base: requestData.id,
|
||||||
|
permissions: {
|
||||||
|
can_read: true,
|
||||||
|
can_edit: requestData.accessType === '编辑权限',
|
||||||
|
can_delete: false,
|
||||||
|
},
|
||||||
|
reason: requestData.reason,
|
||||||
|
expires_at: calculateExpiresAt(requestData.duration),
|
||||||
|
};
|
||||||
|
|
||||||
|
return post('/permissions/', apiRequestData);
|
||||||
|
};
|
@ -1,13 +0,0 @@
|
|||||||
// 模拟的当前用户数据
|
|
||||||
export const mockCurrentUser = {
|
|
||||||
id: 'user-001',
|
|
||||||
username: 'johndoe',
|
|
||||||
email: 'john@example.com',
|
|
||||||
name: 'John Doe',
|
|
||||||
department: '研发部',
|
|
||||||
group: '前端开发组',
|
|
||||||
role: 'developer',
|
|
||||||
avatar: 'https://via.placeholder.com/150',
|
|
||||||
created_at: '2023-01-15T08:30:00Z',
|
|
||||||
updated_at: '2023-12-20T14:45:00Z',
|
|
||||||
};
|
|
@ -1,6 +1,5 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
||||||
import { mockCurrentUser } from './auth.mock';
|
|
||||||
|
|
||||||
const setPending = (state) => {
|
const setPending = (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
@ -24,7 +23,7 @@ const authSlice = createSlice({
|
|||||||
initialState: {
|
initialState: {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
user: mockCurrentUser, // 使用模拟的当前用户数据
|
user: null,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
login: (state, action) => {
|
login: (state, action) => {
|
||||||
|
@ -54,7 +54,8 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje
|
|||||||
|
|
||||||
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
|
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
const { user, message } = await post('/auth/verify-token/');
|
const { data, message } = await post('/auth/verify-token/');
|
||||||
|
const { user } = data;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
throw new Error(message || 'No token found');
|
throw new Error(message || 'No token found');
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
// 模拟聊天历史数据
|
|
||||||
export const mockChatHistory = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: '关于产品开发流程的咨询',
|
|
||||||
knowledge_base_id: '1',
|
|
||||||
knowledge_base_name: '产品开发知识库',
|
|
||||||
created_at: '2025-03-10T10:30:00Z',
|
|
||||||
updated_at: '2025-03-10T11:45:00Z',
|
|
||||||
message_count: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: '市场分析报告查询',
|
|
||||||
knowledge_base_id: '2',
|
|
||||||
knowledge_base_name: '市场分析知识库',
|
|
||||||
created_at: '2025-03-09T14:20:00Z',
|
|
||||||
updated_at: '2025-03-09T15:10:00Z',
|
|
||||||
message_count: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: '技术架构设计讨论',
|
|
||||||
knowledge_base_id: '4',
|
|
||||||
knowledge_base_name: '技术架构知识库',
|
|
||||||
created_at: '2025-03-08T09:15:00Z',
|
|
||||||
updated_at: '2025-03-08T10:30:00Z',
|
|
||||||
message_count: 12,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: '用户反馈分析',
|
|
||||||
knowledge_base_id: '5',
|
|
||||||
knowledge_base_name: '用户研究知识库',
|
|
||||||
created_at: '2025-03-07T16:40:00Z',
|
|
||||||
updated_at: '2025-03-07T17:25:00Z',
|
|
||||||
message_count: 6,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 内存存储,用于模拟数据库操作
|
|
||||||
let chatHistoryStore = [...mockChatHistory];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟获取聊天历史列表
|
|
||||||
* @param {Object} params - 查询参数
|
|
||||||
* @returns {Object} - 分页结果
|
|
||||||
*/
|
|
||||||
export const mockGetChatHistory = (params = { page: 1, page_size: 10 }) => {
|
|
||||||
const { page, page_size } = params;
|
|
||||||
const startIndex = (page - 1) * page_size;
|
|
||||||
const endIndex = startIndex + page_size;
|
|
||||||
const paginatedItems = chatHistoryStore.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
message: '获取成功',
|
|
||||||
data: {
|
|
||||||
total: chatHistoryStore.length,
|
|
||||||
page,
|
|
||||||
page_size,
|
|
||||||
results: paginatedItems,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟创建新聊天
|
|
||||||
* @param {Object} chatData - 聊天数据
|
|
||||||
* @returns {Object} - 创建结果
|
|
||||||
*/
|
|
||||||
export const mockCreateChat = (chatData) => {
|
|
||||||
const newChat = {
|
|
||||||
id: uuidv4(),
|
|
||||||
title: chatData.title || '新的聊天',
|
|
||||||
knowledge_base_id: chatData.knowledge_base_id,
|
|
||||||
knowledge_base_name: chatData.knowledge_base_name || '未知知识库',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
message_count: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
chatHistoryStore.unshift(newChat);
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
message: '创建成功',
|
|
||||||
data: {
|
|
||||||
chat: newChat,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟更新聊天
|
|
||||||
* @param {string} id - 聊天ID
|
|
||||||
* @param {Object} data - 更新数据
|
|
||||||
* @returns {Object} - 更新结果
|
|
||||||
*/
|
|
||||||
export const mockUpdateChat = (id, data) => {
|
|
||||||
const index = chatHistoryStore.findIndex((chat) => chat.id === id);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
return {
|
|
||||||
code: 404,
|
|
||||||
message: '聊天不存在',
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedChat = {
|
|
||||||
...chatHistoryStore[index],
|
|
||||||
...data,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
chatHistoryStore[index] = updatedChat;
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
message: '更新成功',
|
|
||||||
data: {
|
|
||||||
chat: updatedChat,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟删除聊天
|
|
||||||
* @param {string} id - 聊天ID
|
|
||||||
* @returns {Object} - 删除结果
|
|
||||||
*/
|
|
||||||
export const mockDeleteChat = (id) => {
|
|
||||||
const index = chatHistoryStore.findIndex((chat) => chat.id === id);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
return {
|
|
||||||
code: 404,
|
|
||||||
message: '聊天不存在',
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
chatHistoryStore.splice(index, 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
message: '删除成功',
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,130 +0,0 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
import { fetchChatHistory, createChat, deleteChat, updateChat } from './chatHistory.thunks';
|
|
||||||
|
|
||||||
// 初始状态
|
|
||||||
const initialState = {
|
|
||||||
// 聊天历史列表
|
|
||||||
list: {
|
|
||||||
items: [],
|
|
||||||
total: 0,
|
|
||||||
page: 1,
|
|
||||||
page_size: 10,
|
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 当前聊天
|
|
||||||
currentChat: {
|
|
||||||
data: null,
|
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 操作状态(创建、更新、删除)
|
|
||||||
operations: {
|
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建 slice
|
|
||||||
const chatHistorySlice = createSlice({
|
|
||||||
name: 'chatHistory',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
// 重置操作状态
|
|
||||||
resetOperationStatus: (state) => {
|
|
||||||
state.operations.status = 'idle';
|
|
||||||
state.operations.error = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 重置当前聊天
|
|
||||||
resetCurrentChat: (state) => {
|
|
||||||
state.currentChat.data = null;
|
|
||||||
state.currentChat.status = 'idle';
|
|
||||||
state.currentChat.error = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 设置当前聊天
|
|
||||||
setCurrentChat: (state, action) => {
|
|
||||||
state.currentChat.data = action.payload;
|
|
||||||
state.currentChat.status = 'succeeded';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
// 获取聊天历史
|
|
||||||
builder
|
|
||||||
.addCase(fetchChatHistory.pending, (state) => {
|
|
||||||
state.list.status = 'loading';
|
|
||||||
})
|
|
||||||
.addCase(fetchChatHistory.fulfilled, (state, action) => {
|
|
||||||
state.list.status = 'succeeded';
|
|
||||||
state.list.items = action.payload.results;
|
|
||||||
state.list.total = action.payload.total;
|
|
||||||
state.list.page = action.payload.page;
|
|
||||||
state.list.page_size = action.payload.page_size;
|
|
||||||
})
|
|
||||||
.addCase(fetchChatHistory.rejected, (state, action) => {
|
|
||||||
state.list.status = 'failed';
|
|
||||||
state.list.error = action.payload || action.error.message;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建聊天
|
|
||||||
.addCase(createChat.pending, (state) => {
|
|
||||||
state.operations.status = 'loading';
|
|
||||||
})
|
|
||||||
.addCase(createChat.fulfilled, (state, action) => {
|
|
||||||
state.operations.status = 'succeeded';
|
|
||||||
state.list.items.unshift(action.payload);
|
|
||||||
state.list.total += 1;
|
|
||||||
state.currentChat.data = action.payload;
|
|
||||||
state.currentChat.status = 'succeeded';
|
|
||||||
})
|
|
||||||
.addCase(createChat.rejected, (state, action) => {
|
|
||||||
state.operations.status = 'failed';
|
|
||||||
state.operations.error = action.payload || action.error.message;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 删除聊天
|
|
||||||
.addCase(deleteChat.pending, (state) => {
|
|
||||||
state.operations.status = 'loading';
|
|
||||||
})
|
|
||||||
.addCase(deleteChat.fulfilled, (state, action) => {
|
|
||||||
state.operations.status = 'succeeded';
|
|
||||||
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
|
|
||||||
state.list.total -= 1;
|
|
||||||
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
|
|
||||||
state.currentChat.data = null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addCase(deleteChat.rejected, (state, action) => {
|
|
||||||
state.operations.status = 'failed';
|
|
||||||
state.operations.error = action.payload || action.error.message;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新聊天
|
|
||||||
.addCase(updateChat.pending, (state) => {
|
|
||||||
state.operations.status = 'loading';
|
|
||||||
})
|
|
||||||
.addCase(updateChat.fulfilled, (state, action) => {
|
|
||||||
state.operations.status = 'succeeded';
|
|
||||||
const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
state.list.items[index] = action.payload;
|
|
||||||
}
|
|
||||||
if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
|
|
||||||
state.currentChat.data = action.payload;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addCase(updateChat.rejected, (state, action) => {
|
|
||||||
state.operations.status = 'failed';
|
|
||||||
state.operations.error = action.payload || action.error.message;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导出 actions
|
|
||||||
export const { resetOperationStatus, resetCurrentChat, setCurrentChat } = chatHistorySlice.actions;
|
|
||||||
|
|
||||||
// 导出 reducer
|
|
||||||
export default chatHistorySlice.reducer;
|
|
@ -1,87 +0,0 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import { get, post, put, del } from '../../services/api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取聊天历史列表
|
|
||||||
* @param {Object} params - 查询参数
|
|
||||||
* @param {number} params.page - 页码
|
|
||||||
* @param {number} params.page_size - 每页数量
|
|
||||||
*/
|
|
||||||
export const fetchChatHistory = createAsyncThunk(
|
|
||||||
'chatHistory/fetchChatHistory',
|
|
||||||
async (params = { page: 1, page_size: 10 }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await get('/chat-history/', { params });
|
|
||||||
|
|
||||||
// 处理返回格式
|
|
||||||
if (response.data && response.data.code === 200) {
|
|
||||||
return response.data.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
return rejectWithValue(error.response?.data || 'Failed to fetch chat history');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新聊天
|
|
||||||
* @param {Object} chatData - 聊天数据
|
|
||||||
* @param {string} chatData.knowledge_base_id - 知识库ID
|
|
||||||
* @param {string} chatData.title - 聊天标题
|
|
||||||
*/
|
|
||||||
export const createChat = createAsyncThunk('chatHistory/createChat', async (chatData, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await post('/chat-history/', chatData);
|
|
||||||
|
|
||||||
// 处理返回格式
|
|
||||||
if (response.data && response.data.code === 200) {
|
|
||||||
return response.data.data.chat;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
return rejectWithValue(error.response?.data || 'Failed to create chat');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新聊天
|
|
||||||
* @param {Object} params - 更新参数
|
|
||||||
* @param {string} params.id - 聊天ID
|
|
||||||
* @param {Object} params.data - 更新数据
|
|
||||||
*/
|
|
||||||
export const updateChat = createAsyncThunk('chatHistory/updateChat', async ({ id, data }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await put(`/chat-history/${id}/`, data);
|
|
||||||
|
|
||||||
// 处理返回格式
|
|
||||||
if (response.data && response.data.code === 200) {
|
|
||||||
return response.data.data.chat;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
return rejectWithValue(error.response?.data || 'Failed to update chat');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除聊天
|
|
||||||
* @param {string} id - 聊天ID
|
|
||||||
*/
|
|
||||||
export const deleteChat = createAsyncThunk('chatHistory/deleteChat', async (id, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await del(`/chat-history/${id}/`);
|
|
||||||
|
|
||||||
// 处理返回格式
|
|
||||||
if (response.data && response.data.code === 200) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return id;
|
|
||||||
} catch (error) {
|
|
||||||
return rejectWithValue(error.response?.data || 'Failed to delete chat');
|
|
||||||
}
|
|
||||||
});
|
|
83
src/store/permissions/permissions.slice.js
Normal file
83
src/store/permissions/permissions.slice.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { fetchPermissionsThunk, approvePermissionThunk, rejectPermissionThunk } from './permissions.thunks';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
permissions: {
|
||||||
|
items: [],
|
||||||
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
approveReject: {
|
||||||
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
|
error: null,
|
||||||
|
currentId: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissionsSlice = createSlice({
|
||||||
|
name: 'permissions',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
resetApproveRejectStatus: (state) => {
|
||||||
|
state.approveReject = {
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
currentId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
// 获取权限申请列表
|
||||||
|
builder
|
||||||
|
.addCase(fetchPermissionsThunk.pending, (state) => {
|
||||||
|
state.permissions.status = 'loading';
|
||||||
|
state.permissions.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchPermissionsThunk.fulfilled, (state, action) => {
|
||||||
|
state.permissions.status = 'succeeded';
|
||||||
|
state.permissions.items = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(fetchPermissionsThunk.rejected, (state, action) => {
|
||||||
|
state.permissions.status = 'failed';
|
||||||
|
state.permissions.error = action.payload || '获取权限申请列表失败';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批准权限申请
|
||||||
|
builder
|
||||||
|
.addCase(approvePermissionThunk.pending, (state, action) => {
|
||||||
|
state.approveReject.status = 'loading';
|
||||||
|
state.approveReject.error = null;
|
||||||
|
state.approveReject.currentId = action.meta.arg.id;
|
||||||
|
})
|
||||||
|
.addCase(approvePermissionThunk.fulfilled, (state, action) => {
|
||||||
|
state.approveReject.status = 'succeeded';
|
||||||
|
// 从列表中移除已批准的申请
|
||||||
|
state.permissions.items = state.permissions.items.filter((item) => item.id !== action.meta.arg.id);
|
||||||
|
})
|
||||||
|
.addCase(approvePermissionThunk.rejected, (state, action) => {
|
||||||
|
state.approveReject.status = 'failed';
|
||||||
|
state.approveReject.error = action.payload || '批准权限申请失败';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拒绝权限申请
|
||||||
|
builder
|
||||||
|
.addCase(rejectPermissionThunk.pending, (state, action) => {
|
||||||
|
state.approveReject.status = 'loading';
|
||||||
|
state.approveReject.error = null;
|
||||||
|
state.approveReject.currentId = action.meta.arg.id;
|
||||||
|
})
|
||||||
|
.addCase(rejectPermissionThunk.fulfilled, (state, action) => {
|
||||||
|
state.approveReject.status = 'succeeded';
|
||||||
|
// 从列表中移除已拒绝的申请
|
||||||
|
state.permissions.items = state.permissions.items.filter((item) => item.id !== action.meta.arg.id);
|
||||||
|
})
|
||||||
|
.addCase(rejectPermissionThunk.rejected, (state, action) => {
|
||||||
|
state.approveReject.status = 'failed';
|
||||||
|
state.approveReject.error = action.payload || '拒绝权限申请失败';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { resetApproveRejectStatus } = permissionsSlice.actions;
|
||||||
|
|
||||||
|
export default permissionsSlice.reducer;
|
48
src/store/permissions/permissions.thunks.js
Normal file
48
src/store/permissions/permissions.thunks.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { get, post } from '../../services/api';
|
||||||
|
|
||||||
|
// 获取权限申请列表
|
||||||
|
export const fetchPermissionsThunk = createAsyncThunk(
|
||||||
|
'permissions/fetchPermissions',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await get('/permissions/');
|
||||||
|
return response || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取权限申请列表失败:', error);
|
||||||
|
return rejectWithValue('获取权限申请列表失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 批准权限申请
|
||||||
|
export const approvePermissionThunk = createAsyncThunk(
|
||||||
|
'permissions/approvePermission',
|
||||||
|
async ({ id, responseMessage }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await post(`/permissions/${id}/approve/`, {
|
||||||
|
response_message: responseMessage || '已批准',
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批准权限申请失败:', error);
|
||||||
|
return rejectWithValue('批准权限申请失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 拒绝权限申请
|
||||||
|
export const rejectPermissionThunk = createAsyncThunk(
|
||||||
|
'permissions/rejectPermission',
|
||||||
|
async ({ id, responseMessage }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await post(`/permissions/${id}/reject/`, {
|
||||||
|
response_message: responseMessage || '已拒绝',
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('拒绝权限申请失败:', error);
|
||||||
|
return rejectWithValue('拒绝权限申请失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
@ -5,12 +5,14 @@ import notificationReducer from './notification.slice.js';
|
|||||||
import authReducer from './auth/auth.slice.js';
|
import authReducer from './auth/auth.slice.js';
|
||||||
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
||||||
import chatReducer from './chat/chat.slice.js';
|
import chatReducer from './chat/chat.slice.js';
|
||||||
|
import permissionsReducer from './permissions/permissions.slice.js';
|
||||||
|
|
||||||
const rootRducer = combineReducers({
|
const rootRducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
notification: notificationReducer,
|
notification: notificationReducer,
|
||||||
knowledgeBase: knowledgeBaseReducer,
|
knowledgeBase: knowledgeBaseReducer,
|
||||||
chat: chatReducer,
|
chat: chatReducer,
|
||||||
|
permissions: permissionsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
|
Loading…
Reference in New Issue
Block a user