[dev]pending approve

This commit is contained in:
susie-laptop 2025-03-26 22:09:52 -04:00
parent 56c4878065
commit 4c4bfe7e56
12 changed files with 246 additions and 102 deletions

View File

@ -20,18 +20,29 @@ export default function AccessRequestModal({
isSubmitting = false, isSubmitting = false,
}) { }) {
const [accessRequestData, setAccessRequestData] = useState({ const [accessRequestData, setAccessRequestData] = useState({
accessType: '只读访问', permissions: {
duration: '一周', can_read: true,
can_edit: false,
can_delete: false,
},
duration: '30', // 30
reason: '', reason: '',
}); });
const [accessRequestErrors, setAccessRequestErrors] = useState({}); const [accessRequestErrors, setAccessRequestErrors] = useState({});
const handleAccessRequestInputChange = (e) => { const handleAccessRequestInputChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setAccessRequestData((prev) => ({ if (name === 'duration') {
...prev, setAccessRequestData((prev) => ({
[name]: value, ...prev,
})); [name]: value,
}));
} else if (name === 'reason') {
setAccessRequestData((prev) => ({
...prev,
[name]: value,
}));
}
// Clear error when user types // Clear error when user types
if (accessRequestErrors[name]) { if (accessRequestErrors[name]) {
@ -42,6 +53,17 @@ export default function AccessRequestModal({
} }
}; };
const handlePermissionChange = (permissionType) => {
setAccessRequestData((prev) => ({
...prev,
permissions: {
can_read: true, // true
can_edit: permissionType === '编辑权限',
can_delete: false, //
},
}));
};
const validateAccessRequestForm = () => { const validateAccessRequestForm = () => {
const errors = {}; const errors = {};
@ -59,17 +81,27 @@ export default function AccessRequestModal({
return; return;
} }
//
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + parseInt(accessRequestData.duration));
const expiresAt = expirationDate.toISOString();
// //
onSubmit({ onSubmit({
id: knowledgeBaseId, knowledge_base: knowledgeBaseId,
title: knowledgeBaseTitle, permissions: accessRequestData.permissions,
...accessRequestData, reason: accessRequestData.reason,
expires_at: expiresAt,
}); });
// //
setAccessRequestData({ setAccessRequestData({
accessType: '只读访问', permissions: {
duration: '一周', can_read: true,
can_edit: false,
can_delete: false,
},
duration: '30',
reason: '', reason: '',
}); });
setAccessRequestErrors({}); setAccessRequestErrors({});
@ -123,9 +155,8 @@ export default function AccessRequestModal({
</label> </label>
<select <select
className='form-select' className='form-select'
name='accessType' value={accessRequestData.permissions.can_edit ? '编辑权限' : '只读访问'}
value={accessRequestData.accessType} onChange={(e) => handlePermissionChange(e.target.value)}
onChange={handleAccessRequestInputChange}
> >
<option value='只读访问'>只读访问</option> <option value='只读访问'>只读访问</option>
<option value='编辑权限'>编辑权限</option> <option value='编辑权限'>编辑权限</option>
@ -143,11 +174,13 @@ export default function AccessRequestModal({
value={accessRequestData.duration} value={accessRequestData.duration}
onChange={handleAccessRequestInputChange} onChange={handleAccessRequestInputChange}
> >
<option value='一周'>一周</option> <option value='7'>7</option>
<option value='一个月'>一个月</option> <option value='15'>15</option>
<option value='三个月'>三个月</option> <option value='30'>30</option>
<option value='六个月'>六个月</option> <option value='60'>60</option>
<option value='永久'>永久</option> <option value='90'>90</option>
<option value='180'>180</option>
<option value='365'>365</option>
</select> </select>
</div> </div>
<div className='mb-3'> <div className='mb-3'>

View File

@ -127,7 +127,7 @@ export default function HeaderWithNav() {
<Link <Link
className='dropdown-item' className='dropdown-item'
to='#' to='#'
onClick={() => setShowSettings(true)} // onClick={() => setShowSettings(true)}
> >
个人设置 个人设置
</Link> </Link>

View File

@ -235,8 +235,8 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
<div <div
key={message.id} key={message.id}
className={`d-flex ${ className={`d-flex ${
message.role === 'user' ? 'justify-content-end' : 'justify-content-start' message.role === 'user' ? 'align-item-end' : 'align-item-start'
} mb-3`} } mb-3 flex-column`}
> >
<div <div
className={`chat-message p-3 rounded-3 ${ className={`chat-message p-3 rounded-3 ${
@ -248,9 +248,9 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
}} }}
> >
<div className='message-content'>{message.content}</div> <div className='message-content'>{message.content}</div>
<div className='message-time small text-muted mt-1'> </div>
{message.created_at && new Date(message.created_at).toLocaleTimeString()} <div className='message-time small text-muted mt-1'>
</div> {message.created_at && new Date(message.created_at).toLocaleTimeString()}
</div> </div>
</div> </div>
))} ))}

View File

@ -333,7 +333,7 @@ export default function SettingsTab({ knowledgeBase }) {
/> />
{/* User Permissions Manager */} {/* User Permissions Manager */}
<UserPermissionsManager knowledgeBase={knowledgeBase} /> {/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */}
{/* Delete confirmation modal */} {/* Delete confirmation modal */}
<DeleteConfirmModal <DeleteConfirmModal

View File

@ -260,7 +260,13 @@ const KnowledgeBaseForm = ({
'保存设置' '保存设置'
)} )}
</button> </button>
<button type='button' className='btn btn-danger' onClick={onDelete} disabled={isSubmitting}> <button
type='button'
className='btn btn-danger'
onClick={onDelete}
disabled='true'
// disabled={isSubmitting}
>
删除知识库 删除知识库
</button> </button>
</div> </div>

View File

@ -381,17 +381,10 @@ export default function KnowledgeBase() {
setIsSubmittingRequest(true); setIsSubmittingRequest(true);
try { try {
// 使 // 使 - dispatchthunk
await requestKnowledgeBaseAccess(requestData); await dispatch(requestKnowledgeBaseAccess(requestData)).unwrap();
dispatch( // Close modal after success
showNotification({
message: '权限申请已提交',
type: 'success',
})
);
// Close modal
setShowAccessRequestModal(false); setShowAccessRequestModal(false);
} catch (error) { } catch (error) {
dispatch( dispatch(

View File

@ -18,11 +18,11 @@ export default function PermissionsPage() {
return ( return (
<div className='permissions-container'> <div className='permissions-container'>
<div className='permissions-section mb-4'> <div className='permissions-section mb-4 container my-4'>
<PendingRequests /> <PendingRequests />
</div> </div>
{user && user.role === 'admin' && ( {user && user.role === 'admin' && (
<div className='permissions-section'> <div className='permissions-section container my-4'>
<UserPermissions /> <UserPermissions />
</div> </div>
)} )}

View File

@ -24,7 +24,7 @@ export default function PendingRequests() {
// Redux store // Redux store
const { const {
results: pendingRequests, results: permissionRequests,
status: fetchStatus, status: fetchStatus,
error: fetchError, error: fetchError,
page: currentPage, page: currentPage,
@ -185,32 +185,69 @@ export default function PendingRequests() {
); );
}; };
//
const renderStatusBadge = (status) => {
switch (status) {
case 'approved':
return (
<span
className='badge bg-success-subtle text-success d-flex align-items-center gap-1'
style={{ width: 'fit-content' }}
>
<SvgIcon className='circle-check' />
已批准
</span>
);
case 'rejected':
return (
<span
className='badge bg-danger-subtle text-danger d-flex align-items-center gap-1'
style={{ width: 'fit-content' }}
>
<SvgIcon className='circle-xmark' />
已拒绝
</span>
);
case 'pending':
return (
<span
className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'
style={{ width: 'fit-content' }}
>
待处理
</span>
);
default:
return null;
}
};
// //
if (fetchStatus === 'loading' && pendingRequests.length === 0) { if (fetchStatus === 'loading' && permissionRequests.length === 0) {
return ( return (
<div className='text-center py-5'> <div className='text-center py-5'>
<div className='spinner-border' role='status'> <div className='spinner-border' role='status'>
<span className='visually-hidden'>加载中...</span> <span className='visually-hidden'>加载中...</span>
</div> </div>
<p className='mt-3'>加载待处理申请...</p> <p className='mt-3'>加载权限申请...</p>
</div> </div>
); );
} }
// //
if (fetchStatus === 'failed' && pendingRequests.length === 0) { if (fetchStatus === 'failed' && permissionRequests.length === 0) {
return ( return (
<div className='alert alert-danger' role='alert'> <div className='alert alert-danger' role='alert'>
{fetchError || '获取待处理申请失败'} {fetchError || '获取权限申请失败'}
</div> </div>
); );
} }
// //
if (pendingRequests.length === 0) { if (permissionRequests.length === 0) {
return ( return (
<div className='alert alert-info' role='alert'> <div className='alert alert-info' role='alert'>
暂无待处理的权限申请 暂无权限申请记录
</div> </div>
); );
} }
@ -219,12 +256,14 @@ export default function PendingRequests() {
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'>
{permissionRequests.filter((req) => req.status === 'pending').length}个待处理
</div>
</div> </div>
<div className='pending-requests-list'> <div className='pending-requests-list'>
{pendingRequests.map((request) => ( {permissionRequests.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'>
@ -235,7 +274,10 @@ export default function PendingRequests() {
</div> </div>
<div className='request-content'> <div className='request-content'>
<p className='mb-2'>申请访问{request.knowledge_base.name}</p> <div className='d-flex justify-content-between align-items-start mb-2'>
<p className='mb-0'>申请访问{request.knowledge_base.name}</p>
{renderStatusBadge(request.status)}
</div>
{request.permissions.can_edit ? ( {request.permissions.can_edit ? (
<span <span
@ -264,32 +306,53 @@ export default function PendingRequests() {
</small> </small>
</div> </div>
)} )}
{request.response_message && (
<div className='mt-2'>
<small className='text-muted'>审批意见: {request.response_message}</small>
</div>
)}
</div> </div>
<div className='request-actions'> <div className='request-actions'>
<button {request.status === 'pending' ? (
className='btn btn-outline-danger btn-sm' <>
onClick={(e) => { <button
e.stopPropagation(); className='btn btn-outline-danger btn-sm'
handleDirectProcess(request.id, false); onClick={(e) => {
}} e.stopPropagation();
disabled={processingId === request.id && approveRejectStatus === 'loading'} handleDirectProcess(request.id, false);
> }}
{processingId === request.id && approveRejectStatus === 'loading' && !isApproving disabled={processingId === request.id && approveRejectStatus === 'loading'}
? '处理中...' >
: '拒绝'} {processingId === request.id &&
</button> approveRejectStatus === 'loading' &&
<button !isApproving
className='btn btn-outline-success btn-sm' ? '处理中...'
onClick={(e) => { : '拒绝'}
e.stopPropagation(); </button>
handleDirectProcess(request.id, true); <button
}} className='btn btn-outline-success btn-sm'
disabled={processingId === request.id && approveRejectStatus === 'loading'} onClick={(e) => {
> e.stopPropagation();
{processingId === request.id && approveRejectStatus === 'loading' && isApproving handleDirectProcess(request.id, true);
? '处理中...' }}
: '批准'} disabled={processingId === request.id && approveRejectStatus === 'loading'}
</button> >
{processingId === request.id && approveRejectStatus === 'loading' && isApproving
? '处理中...'
: '批准'}
</button>
</>
) : (
<div className='status-text'>
{request.status === 'approved' ? '已批准' : '已拒绝'}
{request.updated_at && (
<small className='text-muted d-block'>
{new Date(request.updated_at).toLocaleDateString()}
</small>
)}
</div>
)}
</div> </div>
</div> </div>
))} ))}

View File

@ -13,6 +13,7 @@ export default function UserPermissions() {
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [showDetailsModal, setShowDetailsModal] = useState(false); const [showDetailsModal, setShowDetailsModal] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'pending', 'approved', 'rejected'
// //
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -62,6 +63,11 @@ export default function UserPermissions() {
setCurrentPage(1); // setCurrentPage(1); //
}; };
const handleStatusFilterChange = (e) => {
setStatusFilter(e.target.value);
setCurrentPage(1); //
};
const handlePageChange = (page) => { const handlePageChange = (page) => {
if (page > 0 && page <= totalPages) { if (page > 0 && page <= totalPages) {
setCurrentPage(page); setCurrentPage(page);
@ -76,17 +82,27 @@ export default function UserPermissions() {
// //
const getFilteredUsers = () => { const getFilteredUsers = () => {
if (!searchTerm.trim()) { let filtered = users;
return users;
//
if (statusFilter !== 'all') {
filtered = filtered.filter((user) =>
user.permissions.some((permission) => permission.status === statusFilter)
);
} }
return users.filter( //
(user) => if (searchTerm.trim()) {
user.user_info?.username.toLowerCase().includes(searchTerm.toLowerCase()) || filtered = filtered.filter(
user.user_info?.name.toLowerCase().includes(searchTerm.toLowerCase()) || (user) =>
user.user_info?.department.toLowerCase().includes(searchTerm.toLowerCase()) || user.user_info?.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user_info?.role.toLowerCase().includes(searchTerm.toLowerCase()) user.user_info?.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
); user.user_info?.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.user_info?.role.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return filtered;
}; };
const filteredUsers = getFilteredUsers(); const filteredUsers = getFilteredUsers();
@ -196,22 +212,35 @@ export default function UserPermissions() {
</div> </div>
<> <>
{/* <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='d-flex gap-3'>
<div className='input-group'> <div className='search-bar' style={{ maxWidth: '300px' }}>
<span className='input-group-text bg-light border-end-0'> <div className='input-group'>
<SvgIcon className='search' /> <span className='input-group-text bg-light border-end-0'>
</span> <SvgIcon className='search' />
<input </span>
type='text' <input
className='form-control border-start-0' type='text'
placeholder='搜索用户...' className='form-control border-start-0'
value={searchTerm} placeholder='搜索用户...'
onChange={handleSearchChange} value={searchTerm}
/> onChange={handleSearchChange}
/>
</div>
</div> </div>
<select
className='form-select'
value={statusFilter}
onChange={handleStatusFilterChange}
style={{ width: 'auto' }}
>
<option value='all'>全部状态</option>
<option value='pending'>待处理</option>
<option value='approved'>已批准</option>
<option value='rejected'>已拒绝</option>
</select>
</div> </div>
</div> */} </div>
{loading === 'loading' ? ( {loading === 'loading' ? (
<div className='text-center my-5'> <div className='text-center my-5'>

View File

@ -214,4 +214,21 @@ export const switchToRealApi = async () => {
return isServerUp; return isServerUp;
}; };
// 权限相关API
export const applyPermission = (data) => {
return post('/permissions/', data);
};
export const updatePermission = (data) => {
return post('/permissions/update_permission/', data);
};
export const approvePermission = (permissionId) => {
return post(`/permissions/approve_permission/${permissionId}`);
};
export const rejectPermission = (permissionId) => {
return post(`/permissions/reject_permission/${permissionId}`);
};
export { get, post, put, del, upload }; export { get, post, put, del, upload };

View File

@ -31,7 +31,8 @@ export const calculateExpiresAt = (duration) => {
}; };
/** /**
* 申请知识库访问权限 * 申请知识库访问权限已废弃请使用store/knowledgeBase/knowledgeBase.thunks中的requestKnowledgeBaseAccess
* @deprecated 请使用Redux thunk版本
* @param {Object} requestData - 请求数据 * @param {Object} requestData - 请求数据
* @param {string} requestData.id - 知识库ID * @param {string} requestData.id - 知识库ID
* @param {string} requestData.accessType - 访问类型 '只读访问', '编辑权限' * @param {string} requestData.accessType - 访问类型 '只读访问', '编辑权限'
@ -39,7 +40,7 @@ export const calculateExpiresAt = (duration) => {
* @param {string} requestData.reason - 申请原因 * @param {string} requestData.reason - 申请原因
* @returns {Promise} - API 请求的 Promise * @returns {Promise} - API 请求的 Promise
*/ */
export const requestKnowledgeBaseAccess = async (requestData) => { export const legacyRequestKnowledgeBaseAccess = async (requestData) => {
const apiRequestData = { const apiRequestData = {
knowledge_base: requestData.id, knowledge_base: requestData.id,
permissions: { permissions: {

View File

@ -165,15 +165,17 @@ export const changeKnowledgeBaseType = createAsyncThunk(
/** /**
* 申请知识库访问权限 * 申请知识库访问权限
* @param {Object} params - 参数 * @param {Object} params - 参数
* @param {string} params.knowledgeBaseId - 知识库ID * @param {string} params.knowledge_base - 知识库ID
* @param {Object} params.permissions - 权限对象
* @param {string} params.reason - 申请原因
* @param {string} params.expires_at - 过期时间
* @returns {Promise} - Promise对象 * @returns {Promise} - Promise对象
*/ */
export const requestKnowledgeBaseAccess = createAsyncThunk( export const requestKnowledgeBaseAccess = createAsyncThunk(
'knowledgeBase/requestAccess', 'knowledgeBase/requestAccess',
async (params, { rejectWithValue, dispatch }) => { async (params, { rejectWithValue, dispatch }) => {
try { try {
const { knowledgeBaseId } = params; const response = await post('/permissions/', params);
const response = await post(`/knowledge-bases/${knowledgeBaseId}/request_access/`);
dispatch( dispatch(
showNotification({ showNotification({
type: 'success', type: 'success',