mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 04:38:14 +08:00
484 lines
18 KiB
JavaScript
484 lines
18 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { showNotification } from '../../store/notification.slice';
|
|
import {
|
|
fetchKnowledgeBases,
|
|
searchKnowledgeBases,
|
|
createKnowledgeBase,
|
|
} from '../../store/knowledgeBase/knowledgeBase.thunks';
|
|
import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice';
|
|
import SvgIcon from '../../components/SvgIcon';
|
|
|
|
// 导入拆分的组件
|
|
import SearchBar from './components/SearchBar';
|
|
import Pagination from './components/Pagination';
|
|
import CreateKnowledgeBaseModal from './components/CreateKnowledgeBaseModal';
|
|
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
|
|
|
export default function KnowledgeBase() {
|
|
const dispatch = useDispatch();
|
|
const navigate = useNavigate();
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
|
|
const [formErrors, setFormErrors] = useState({});
|
|
const [accessRequestErrors, setAccessRequestErrors] = useState({});
|
|
const [accessRequestData, setAccessRequestData] = useState({
|
|
id: '',
|
|
title: '',
|
|
accessType: '只读访问',
|
|
duration: '一周',
|
|
projectInfo: '',
|
|
reason: '',
|
|
});
|
|
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
|
|
name: '',
|
|
desc: '',
|
|
});
|
|
|
|
// Search state
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
|
|
// Pagination state
|
|
const [pagination, setPagination] = useState({
|
|
page: 1,
|
|
page_size: 10,
|
|
});
|
|
|
|
// Get knowledge bases from Redux store
|
|
const { items: knowledgeBases, total, status, error } = useSelector((state) => state.knowledgeBase.list);
|
|
const {
|
|
items: searchResults,
|
|
total: searchTotal,
|
|
status: searchStatus,
|
|
error: searchError,
|
|
keyword: storeKeyword,
|
|
} = useSelector((state) => state.knowledgeBase.search);
|
|
const { status: operationStatus, error: operationError } = useSelector((state) => state.knowledgeBase.operations);
|
|
|
|
// Determine which data to display based on search state
|
|
const displayData = isSearching ? searchResults : knowledgeBases;
|
|
const displayTotal = isSearching ? searchTotal : total;
|
|
const displayStatus = isSearching ? searchStatus : status;
|
|
const displayError = isSearching ? searchError : error;
|
|
|
|
// Fetch knowledge bases when component mounts or pagination changes
|
|
useEffect(() => {
|
|
if (!isSearching) {
|
|
dispatch(fetchKnowledgeBases(pagination));
|
|
} else if (searchKeyword.trim()) {
|
|
dispatch(
|
|
searchKnowledgeBases({
|
|
keyword: searchKeyword,
|
|
page: pagination.page,
|
|
page_size: pagination.page_size,
|
|
})
|
|
);
|
|
}
|
|
}, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]);
|
|
|
|
// Handle search input change
|
|
const handleSearchInputChange = (e) => {
|
|
setSearchKeyword(e.target.value);
|
|
};
|
|
|
|
// Handle search submit
|
|
const handleSearch = (e) => {
|
|
e.preventDefault();
|
|
|
|
if (searchKeyword.trim()) {
|
|
setIsSearching(true);
|
|
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
|
|
dispatch(
|
|
searchKnowledgeBases({
|
|
keyword: searchKeyword,
|
|
page: 1,
|
|
page_size: pagination.page_size,
|
|
})
|
|
);
|
|
} else {
|
|
// If search is empty, reset to normal list view
|
|
handleClearSearch();
|
|
}
|
|
};
|
|
|
|
// Handle clear search
|
|
const handleClearSearch = () => {
|
|
setSearchKeyword('');
|
|
setIsSearching(false);
|
|
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
|
|
dispatch(resetSearchState());
|
|
};
|
|
|
|
// Show loading state while fetching data
|
|
const isLoading = displayStatus === 'loading';
|
|
|
|
// Show error notification if fetch fails
|
|
useEffect(() => {
|
|
if (displayStatus === 'failed' && displayError) {
|
|
dispatch(
|
|
showNotification({
|
|
message: `获取知识库列表失败: ${displayError.message || displayError}`,
|
|
type: 'danger',
|
|
})
|
|
);
|
|
}
|
|
}, [displayStatus, displayError, dispatch]);
|
|
|
|
// Show notification for operation status
|
|
useEffect(() => {
|
|
if (operationStatus === 'succeeded') {
|
|
dispatch(
|
|
showNotification({
|
|
message: '操作成功',
|
|
type: 'success',
|
|
})
|
|
);
|
|
// Refresh the list after successful operation
|
|
if (isSearching && searchKeyword.trim()) {
|
|
dispatch(
|
|
searchKnowledgeBases({
|
|
keyword: searchKeyword,
|
|
page: pagination.page,
|
|
page_size: pagination.page_size,
|
|
})
|
|
);
|
|
} else {
|
|
dispatch(fetchKnowledgeBases(pagination));
|
|
}
|
|
} else if (operationStatus === 'failed' && operationError) {
|
|
dispatch(
|
|
showNotification({
|
|
message: `操作失败: ${operationError.message || operationError}`,
|
|
type: 'danger',
|
|
})
|
|
);
|
|
}
|
|
}, [operationStatus, operationError, dispatch, pagination, isSearching, searchKeyword]);
|
|
|
|
// Handle pagination change
|
|
const handlePageChange = (newPage) => {
|
|
setPagination((prev) => ({
|
|
...prev,
|
|
page: newPage,
|
|
}));
|
|
};
|
|
|
|
// Handle page size change
|
|
const handlePageSizeChange = (newPageSize) => {
|
|
setPagination({
|
|
page: 1, // Reset to first page when changing page size
|
|
page_size: newPageSize,
|
|
});
|
|
};
|
|
|
|
const handleInputChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setNewKnowledgeBase((prev) => ({
|
|
...prev,
|
|
[name]: value,
|
|
}));
|
|
|
|
// Clear error when user types
|
|
if (formErrors[name]) {
|
|
setFormErrors((prev) => ({
|
|
...prev,
|
|
[name]: '',
|
|
}));
|
|
}
|
|
};
|
|
|
|
const handleAccessRequestInputChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setAccessRequestData((prev) => ({
|
|
...prev,
|
|
[name]: value,
|
|
}));
|
|
|
|
// Clear error when user types
|
|
if (accessRequestErrors[name]) {
|
|
setAccessRequestErrors((prev) => ({
|
|
...prev,
|
|
[name]: '',
|
|
}));
|
|
}
|
|
};
|
|
|
|
const validateCreateForm = () => {
|
|
const errors = {};
|
|
|
|
if (!newKnowledgeBase.name.trim()) {
|
|
errors.name = '请输入知识库名称';
|
|
}
|
|
|
|
if (!newKnowledgeBase.desc.trim()) {
|
|
errors.desc = '请输入知识库描述';
|
|
}
|
|
|
|
setFormErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const validateAccessRequestForm = () => {
|
|
const errors = {};
|
|
|
|
if (!accessRequestData.projectInfo.trim()) {
|
|
errors.projectInfo = '请输入项目信息';
|
|
}
|
|
|
|
if (!accessRequestData.reason.trim()) {
|
|
errors.reason = '请输入申请原因';
|
|
}
|
|
|
|
setAccessRequestErrors(errors);
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
const handleCreateKnowledgeBase = () => {
|
|
// Validate form
|
|
if (!validateCreateForm()) {
|
|
return;
|
|
}
|
|
|
|
// Dispatch create knowledge base action
|
|
dispatch(
|
|
createKnowledgeBase({
|
|
name: newKnowledgeBase.name,
|
|
desc: newKnowledgeBase.desc,
|
|
type: 'private', // Default type
|
|
})
|
|
);
|
|
|
|
// Reset form and close modal
|
|
setNewKnowledgeBase({ name: '', desc: '' });
|
|
setFormErrors({});
|
|
setShowCreateModal(false);
|
|
};
|
|
|
|
// Handle card click to navigate to knowledge base detail
|
|
const handleCardClick = (id) => {
|
|
navigate(`/knowledge-base/${id}/datasets`);
|
|
};
|
|
|
|
const handleRequestAccess = (id, title) => {
|
|
setAccessRequestData((prev) => ({
|
|
...prev,
|
|
id,
|
|
title,
|
|
}));
|
|
setAccessRequestErrors({});
|
|
setShowAccessRequestModal(true);
|
|
};
|
|
|
|
const handleSubmitAccessRequest = () => {
|
|
// Validate form
|
|
if (!validateAccessRequestForm()) {
|
|
return;
|
|
}
|
|
|
|
// Here you would typically call an API to submit the access request
|
|
dispatch(
|
|
showNotification({
|
|
message: '权限申请已提交',
|
|
type: 'success',
|
|
})
|
|
);
|
|
|
|
// Reset form and close modal
|
|
setAccessRequestData((prev) => ({
|
|
...prev,
|
|
projectInfo: '',
|
|
reason: '',
|
|
}));
|
|
setAccessRequestErrors({});
|
|
setShowAccessRequestModal(false);
|
|
};
|
|
|
|
// Calculate total pages
|
|
const totalPages = Math.ceil(displayTotal / pagination.page_size);
|
|
|
|
return (
|
|
<div className='knowledge-base container mt-4'>
|
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
|
<SearchBar
|
|
searchKeyword={searchKeyword}
|
|
isSearching={isSearching}
|
|
onSearchChange={handleSearchInputChange}
|
|
onSearch={handleSearch}
|
|
onClearSearch={handleClearSearch}
|
|
/>
|
|
<button
|
|
className='btn btn-dark d-flex align-items-center gap-1'
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
<SvgIcon className={'plus'} />
|
|
新建知识库
|
|
</button>
|
|
</div>
|
|
|
|
{isSearching && (
|
|
<div className='alert alert-info'>
|
|
搜索结果: "{storeKeyword}" - 找到 {displayTotal} 个知识库
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<div className='d-flex justify-content-center my-5'>
|
|
<div className='spinner-border' role='status'>
|
|
<span className='visually-hidden'>加载中...</span>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<KnowledgeBaseList
|
|
knowledgeBases={displayData}
|
|
isSearching={isSearching}
|
|
onCardClick={handleCardClick}
|
|
onRequestAccess={handleRequestAccess}
|
|
/>
|
|
|
|
{/* Pagination */}
|
|
{totalPages > 1 && (
|
|
<Pagination
|
|
currentPage={pagination.page}
|
|
totalPages={totalPages}
|
|
pageSize={pagination.page_size}
|
|
onPageChange={handlePageChange}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 新建知识库弹窗 */}
|
|
<CreateKnowledgeBaseModal
|
|
show={showCreateModal}
|
|
formData={newKnowledgeBase}
|
|
formErrors={formErrors}
|
|
isSubmitting={operationStatus === 'loading'}
|
|
onClose={() => setShowCreateModal(false)}
|
|
onChange={handleInputChange}
|
|
onSubmit={handleCreateKnowledgeBase}
|
|
/>
|
|
|
|
{/* 申请权限弹窗 */}
|
|
{showAccessRequestModal && (
|
|
<div
|
|
className='modal-backdrop'
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 1050,
|
|
}}
|
|
>
|
|
<div
|
|
className='modal-content bg-white rounded shadow'
|
|
style={{
|
|
width: '500px',
|
|
maxWidth: '90%',
|
|
padding: '20px',
|
|
}}
|
|
>
|
|
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
|
<h5 className='modal-title m-0'>申请访问权限</h5>
|
|
<button
|
|
type='button'
|
|
className='btn-close'
|
|
onClick={() => setShowAccessRequestModal(false)}
|
|
aria-label='Close'
|
|
></button>
|
|
</div>
|
|
<div className='modal-body'>
|
|
<div className='mb-3'>
|
|
<label className='form-label'>知识库名称</label>
|
|
<input type='text' className='form-control' value={accessRequestData.title} readOnly />
|
|
</div>
|
|
<div className='mb-3'>
|
|
<label className='form-label'>权限类型</label>
|
|
<select
|
|
className='form-select'
|
|
name='accessType'
|
|
value={accessRequestData.accessType}
|
|
onChange={handleAccessRequestInputChange}
|
|
>
|
|
<option value='只读访问'>只读访问</option>
|
|
<option value='编辑权限'>编辑权限</option>
|
|
</select>
|
|
</div>
|
|
<div className='mb-3'>
|
|
<label className='form-label'>访问时长</label>
|
|
<select
|
|
className='form-select'
|
|
name='duration'
|
|
value={accessRequestData.duration}
|
|
onChange={handleAccessRequestInputChange}
|
|
>
|
|
<option value='一周'>一周</option>
|
|
<option value='一个月'>一个月</option>
|
|
<option value='三个月'>三个月</option>
|
|
<option value='六个月'>六个月</option>
|
|
<option value='永久'>永久</option>
|
|
</select>
|
|
</div>
|
|
<div className='mb-3'>
|
|
<label htmlFor='projectInfo' className='form-label'>
|
|
项目信息 <span className='text-danger'>*</span>
|
|
</label>
|
|
<input
|
|
type='text'
|
|
className={`form-control ${accessRequestErrors.projectInfo ? 'is-invalid' : ''}`}
|
|
id='projectInfo'
|
|
name='projectInfo'
|
|
value={accessRequestData.projectInfo}
|
|
onChange={handleAccessRequestInputChange}
|
|
placeholder='请输入项目信息'
|
|
/>
|
|
{accessRequestErrors.projectInfo && (
|
|
<div className='invalid-feedback'>{accessRequestErrors.projectInfo}</div>
|
|
)}
|
|
</div>
|
|
<div className='mb-3'>
|
|
<label htmlFor='reason' className='form-label'>
|
|
申请原因 <span className='text-danger'>*</span>
|
|
</label>
|
|
<textarea
|
|
className={`form-control ${accessRequestErrors.reason ? 'is-invalid' : ''}`}
|
|
id='reason'
|
|
name='reason'
|
|
rows='3'
|
|
value={accessRequestData.reason}
|
|
onChange={handleAccessRequestInputChange}
|
|
placeholder='请输入申请原因'
|
|
></textarea>
|
|
{accessRequestErrors.reason && (
|
|
<div className='invalid-feedback'>{accessRequestErrors.reason}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className='modal-footer'>
|
|
<button
|
|
type='button'
|
|
className='btn btn-secondary'
|
|
onClick={() => setShowAccessRequestModal(false)}
|
|
>
|
|
取消
|
|
</button>
|
|
<button type='button' className='btn btn-primary' onClick={handleSubmitAccessRequest}>
|
|
提交申请
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|