mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 04:58:13 +08:00
[dev]split components & add api
This commit is contained in:
parent
843df8802a
commit
6c1f0f3166
@ -101,4 +101,5 @@ export const icons = {
|
||||
chat: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M123.6 391.3c12.9-9.4 29.6-11.8 44.6-6.4c26.5 9.6 56.2 15.1 87.8 15.1c124.7 0 208-80.5 208-160s-83.3-160-208-160S48 160.5 48 240c0 32 12.4 62.8 35.7 89.2c8.6 9.7 12.8 22.5 11.8 35.5c-1.4 18.1-5.7 34.7-11.3 49.4c17-7.9 31.1-16.7 39.4-22.7zM21.2 431.9c1.8-2.7 3.5-5.4 5.1-8.1c10-16.6 19.5-38.4 21.4-62.9C17.7 326.8 0 285.1 0 240C0 125.1 114.6 32 256 32s256 93.1 256 208s-114.6 208-256 208c-37.1 0-72.3-6.4-104.1-17.9c-11.9 8.7-31.3 20.6-54.3 30.6c-15.1 6.6-32.3 12.6-50.1 16.1c-.8 .2-1.6 .3-2.4 .5c-4.4 .8-8.7 1.5-13.2 1.9c-.2 0-.5 .1-.7 .1c-5.1 .5-10.2 .8-15.3 .8c-6.5 0-12.3-3.9-14.8-9.9c-2.5-6-1.1-12.8 3.4-17.4c4.1-4.2 7.8-8.7 11.3-13.5c1.7-2.3 3.3-4.6 4.8-6.9l.3-.5z"/></svg>`,
|
||||
'arrowup-upload': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill='currentColor'><path d="M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3 192 320c0 17.7 14.3 32 32 32s32-14.3 32-32l0-210.7 73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 17.7-14.3 32-32 32L96 448c-17.7 0-32-14.3-32-32l0-64z"/></svg>`,
|
||||
send: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z"/></svg>`,
|
||||
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>`
|
||||
};
|
||||
|
@ -1,13 +1,21 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showNotification } from '../../../store/notification.slice';
|
||||
import SvgIcon from '../../../components/SvgIcon';
|
||||
|
||||
// 导入拆分的组件
|
||||
import Breadcrumb from './components/Breadcrumb';
|
||||
import DocumentList from './components/DocumentList';
|
||||
import FileUploadModal from './components/FileUploadModal';
|
||||
|
||||
export default function DatasetTab({ knowledgeBase }) {
|
||||
const dispatch = useDispatch();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
||||
const [selectAll, setSelectAll] = useState(false);
|
||||
const [showBatchDropdown, setShowBatchDropdown] = useState(false);
|
||||
const [showAddFileModal, setShowAddFileModal] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [newFile, setNewFile] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
@ -17,23 +25,13 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
const dropdownRef = useRef(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// Convert documents to state so we can update it
|
||||
const [documents, setDocuments] = useState([
|
||||
{
|
||||
id: '1001',
|
||||
name: '测试数据集 001',
|
||||
description: '产品相关的所有文档和说明',
|
||||
size: '124kb',
|
||||
updatedAt: '2023-05-15',
|
||||
},
|
||||
{
|
||||
id: '1002',
|
||||
name: '产品分析数据',
|
||||
description: '技术架构和API文档',
|
||||
size: '89kb',
|
||||
updatedAt: '2023-05-10',
|
||||
},
|
||||
]);
|
||||
// Use documents from knowledge base or empty array if not available
|
||||
const [documents, setDocuments] = useState(knowledgeBase.documents || []);
|
||||
|
||||
// Update documents when knowledgeBase changes
|
||||
useEffect(() => {
|
||||
setDocuments(knowledgeBase.documents || []);
|
||||
}, [knowledgeBase]);
|
||||
|
||||
// Handle click outside dropdown
|
||||
useEffect(() => {
|
||||
@ -51,6 +49,11 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
};
|
||||
}, [dropdownRef]);
|
||||
|
||||
// Handle search input change
|
||||
const handleSearchChange = (e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
};
|
||||
|
||||
// Handle select all checkbox
|
||||
const handleSelectAll = () => {
|
||||
if (selectAll) {
|
||||
@ -84,6 +87,14 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
// Update documents state by removing selected documents
|
||||
setDocuments((prevDocuments) => prevDocuments.filter((doc) => !selectedDocuments.includes(doc.id)));
|
||||
|
||||
// Show notification
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '已删除选中的数据集',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
// Reset selection
|
||||
setSelectedDocuments([]);
|
||||
setSelectAll(false);
|
||||
@ -166,35 +177,48 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Here you would typically call an API to upload the file
|
||||
console.log('Uploading file:', newFile);
|
||||
|
||||
// Generate a new ID for the document
|
||||
const newId = (Math.max(...documents.map((doc) => parseInt(doc.id)), 0) + 1).toString();
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
// Generate a new ID for the document
|
||||
const newId = Date.now().toString();
|
||||
|
||||
// Format file size
|
||||
const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb';
|
||||
// Format file size
|
||||
const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb';
|
||||
|
||||
// Get current date
|
||||
const today = new Date();
|
||||
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(
|
||||
today.getDate()
|
||||
).padStart(2, '0')}`;
|
||||
// Get current date
|
||||
const today = new Date();
|
||||
const formattedDate = today.toISOString();
|
||||
|
||||
// Create new document object
|
||||
const newDocument = {
|
||||
id: newId,
|
||||
name: newFile.name,
|
||||
description: newFile.description || '无描述',
|
||||
size: fileSizeKB,
|
||||
updatedAt: formattedDate,
|
||||
};
|
||||
// Create new document object
|
||||
const newDocument = {
|
||||
id: newId,
|
||||
name: newFile.name,
|
||||
description: newFile.description || '无描述',
|
||||
size: fileSizeKB,
|
||||
create_time: formattedDate,
|
||||
update_time: formattedDate,
|
||||
};
|
||||
|
||||
// Add new document to the documents array
|
||||
setDocuments((prevDocuments) => [...prevDocuments, newDocument]);
|
||||
// Add new document to the documents array
|
||||
setDocuments((prevDocuments) => [...prevDocuments, newDocument]);
|
||||
|
||||
// Reset form and close modal
|
||||
handleCloseAddFileModal();
|
||||
// Show notification
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '数据集上传成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
setIsSubmitting(false);
|
||||
// Reset form and close modal
|
||||
handleCloseAddFileModal();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// Open file selector when clicking on the upload area
|
||||
@ -220,299 +244,103 @@ export default function DatasetTab({ knowledgeBase }) {
|
||||
|
||||
// Update documents state by removing the deleted document
|
||||
setDocuments((prevDocuments) => prevDocuments.filter((doc) => doc.id !== docId));
|
||||
|
||||
// Show notification
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '数据集已删除',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Filter documents based on search query
|
||||
const filteredDocuments = documents.filter(
|
||||
(doc) =>
|
||||
doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(doc.description && doc.description.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Breadcrumb navigation */}
|
||||
<div className='d-flex align-items-center mb-4 mt-3'>
|
||||
<nav aria-label='breadcrumb'>
|
||||
<ol className='breadcrumb mb-0'>
|
||||
<li className='breadcrumb-item'>
|
||||
<Link className='text-secondary text-decoration-none' to='/'>
|
||||
知识库
|
||||
</Link>
|
||||
</li>
|
||||
<li className='breadcrumb-item'>
|
||||
<Link
|
||||
className='text-secondary text-decoration-none'
|
||||
to={`/knowledge-base/${knowledgeBase.id}`}
|
||||
>
|
||||
{knowledgeBase.title}
|
||||
</Link>
|
||||
</li>
|
||||
<li className='breadcrumb-item active text-dark' aria-current='page'>
|
||||
数据集
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<Breadcrumb knowledgeBase={knowledgeBase} activeTab='datasets' />
|
||||
|
||||
{/* Action bar */}
|
||||
{/* Toolbar */}
|
||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className='dropdown me-2 position-relative' ref={dropdownRef}>
|
||||
<button
|
||||
className={`btn ${
|
||||
selectedDocuments.length > 0 ? 'btn-outline-dark' : 'btn-outline-secondary'
|
||||
} dropdown-toggle d-flex align-items-center gap-1`}
|
||||
type='button'
|
||||
onClick={() => setShowBatchDropdown(!showBatchDropdown)}
|
||||
aria-expanded={showBatchDropdown}
|
||||
disabled={selectedDocuments.length === 0}
|
||||
>
|
||||
<SvgIcon
|
||||
className='stack-fill'
|
||||
width='18'
|
||||
height='18'
|
||||
color={selectedDocuments.length > 0 ? '#212529' : '#6c757d'}
|
||||
/>
|
||||
批量操作 {selectedDocuments.length > 0 ? `(${selectedDocuments.length})` : ''}
|
||||
</button>
|
||||
{showBatchDropdown && (
|
||||
<ul className='dropdown-menu shadow show' style={{ position: 'absolute', zIndex: 1000 }}>
|
||||
<li>
|
||||
<button
|
||||
className='dropdown-item d-flex align-items-center text-danger'
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
<SvgIcon className='trash' width='16' height='16' color='#dc3545' />
|
||||
<span className='ms-2'>删除</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className='position-relative'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
placeholder='搜索文件...'
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<span className='position-absolute top-50 end-0 translate-middle-y pe-3'>
|
||||
<SvgIcon className='search' color='#6c757d' />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-dark d-flex align-items-center gap-1'
|
||||
onClick={() => setShowAddFileModal(true)}
|
||||
>
|
||||
<SvgIcon className='plus' color='#ffffff' />
|
||||
新增文件
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Documents table */}
|
||||
<div className='card border-0 shadow-sm'>
|
||||
<div className='card-body p-0'>
|
||||
<table className='table table-hover mb-0'>
|
||||
<thead className='table-light'>
|
||||
<tr>
|
||||
<th scope='col' width='40'>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
id='selectAllCheckbox'
|
||||
checked={selectAll}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th scope='col'>ID</th>
|
||||
<th scope='col'>名称</th>
|
||||
<th scope='col'>描述</th>
|
||||
<th scope='col'>文档大小</th>
|
||||
<th scope='col'>更新日期</th>
|
||||
<th scope='col'>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((doc) => (
|
||||
<tr key={doc.id}>
|
||||
<td>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
id={`checkbox-${doc.id}`}
|
||||
checked={selectedDocuments.includes(doc.id)}
|
||||
onChange={() => handleSelectDocument(doc.id)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>#{doc.id}</td>
|
||||
<td>{doc.name}</td>
|
||||
<td>{doc.description}</td>
|
||||
<td>{doc.size}</td>
|
||||
<td>{doc.updatedAt}</td>
|
||||
<td>
|
||||
<div className='d-flex gap-1'>
|
||||
<button className='btn btn-sm text-primary' title='查看'>
|
||||
<SvgIcon className='eye' width='18' height='18' />
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm text-danger'
|
||||
title='删除'
|
||||
onClick={() => handleDeleteDocument(doc.id)}
|
||||
>
|
||||
<SvgIcon className='trash' width='18' height='18' />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||
<div>
|
||||
每页行数:
|
||||
<select className='form-select form-select d-inline-block ms-2' style={{ width: '70px' }}>
|
||||
<option value='5'>5</option>
|
||||
<option value='10'>10</option>
|
||||
<option value='20'>20</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='d-flex align-items-center'>
|
||||
<span className='me-3'>1-5 of 10</span>
|
||||
<nav aria-label='Page navigation'>
|
||||
<ul className='pagination pagination mb-0'>
|
||||
<li className='page-item'>
|
||||
<button className='page-link' aria-label='Previous'>
|
||||
<span aria-hidden='true'>«</span>
|
||||
</button>
|
||||
</li>
|
||||
<li className='page-item'>
|
||||
<button className='page-link' aria-label='Next'>
|
||||
<span aria-hidden='true'>»</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add File Modal */}
|
||||
{showAddFileModal && (
|
||||
<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='d-flex gap-2'>
|
||||
<button
|
||||
className='btn btn-primary d-flex align-items-center gap-1'
|
||||
onClick={() => setShowAddFileModal(true)}
|
||||
>
|
||||
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
||||
<h5 className='modal-title m-0'>新增文件</h5>
|
||||
<SvgIcon className='plus' />
|
||||
上传文件
|
||||
</button>
|
||||
{selectedDocuments.length > 0 && (
|
||||
<div className='dropdown' ref={dropdownRef}>
|
||||
<button
|
||||
className='btn btn-outline-secondary dropdown-toggle'
|
||||
type='button'
|
||||
className='btn-close'
|
||||
onClick={handleCloseAddFileModal}
|
||||
aria-label='Close'
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='fileName' className='form-label'>
|
||||
文件名称 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control bg-light'
|
||||
id='fileName'
|
||||
value={newFile.name}
|
||||
placeholder='在此输入文件名称'
|
||||
readOnly
|
||||
/>
|
||||
<small className='text-muted'>文件名称将自动填充为上传文件的名称</small>
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='fileDescription' className='form-label'>
|
||||
文件描述
|
||||
</label>
|
||||
<textarea
|
||||
className='form-control'
|
||||
id='fileDescription'
|
||||
value={newFile.description}
|
||||
onChange={handleDescriptionChange}
|
||||
placeholder='在此输入文件描述...'
|
||||
rows='3'
|
||||
></textarea>
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>
|
||||
上传文件 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<div
|
||||
className={`border rounded p-4 text-center ${
|
||||
fileErrors.file ? 'border-danger' : 'border-dashed'
|
||||
}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={handleUploadAreaClick}
|
||||
onDrop={handleFileDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
<input
|
||||
type='file'
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
{newFile.file ? (
|
||||
<div>
|
||||
<p className='mb-1 fw-bold'>{newFile.name}</p>
|
||||
<p className='text-muted mb-0'>
|
||||
{(newFile.file.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<SvgIcon className='arrowup-upload' width='36' height='36' />
|
||||
<p className='my-1'>点击或拖拽文件到此处上传</p>
|
||||
<p className='text-muted mb-0'>支持 PDF, DOCX, TXT, CSV 等格式</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{fileErrors.file && <div className='text-danger small mt-1'>{fileErrors.file}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer d-flex justify-content-end gap-2'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-outline-secondary'
|
||||
onClick={handleCloseAddFileModal}
|
||||
onClick={() => setShowBatchDropdown(!showBatchDropdown)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark' onClick={handleFileUpload}>
|
||||
确定
|
||||
批量操作 ({selectedDocuments.length})
|
||||
</button>
|
||||
{showBatchDropdown && (
|
||||
<ul
|
||||
className='dropdown-menu show'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '0px auto auto 0px',
|
||||
margin: '0px',
|
||||
transform: 'translate(0px, 40px)',
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<button className='dropdown-item text-danger' onClick={handleBatchDelete}>
|
||||
<SvgIcon className='trash' />
|
||||
<span className='ms-1'>删除所选</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='w-25'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
placeholder='搜索数据集...'
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document list */}
|
||||
<DocumentList
|
||||
documents={filteredDocuments}
|
||||
selectedDocuments={selectedDocuments}
|
||||
selectAll={selectAll}
|
||||
onSelectAll={handleSelectAll}
|
||||
onSelectDocument={handleSelectDocument}
|
||||
onDeleteDocument={handleDeleteDocument}
|
||||
/>
|
||||
|
||||
{/* File upload modal */}
|
||||
<FileUploadModal
|
||||
show={showAddFileModal}
|
||||
newFile={newFile}
|
||||
fileErrors={fileErrors}
|
||||
isSubmitting={isSubmitting}
|
||||
onClose={handleCloseAddFileModal}
|
||||
onDescriptionChange={handleDescriptionChange}
|
||||
onFileChange={handleFileChange}
|
||||
onFileDrop={handleFileDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onUploadAreaClick={handleUploadAreaClick}
|
||||
onUpload={handleFileUpload}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import SvgIcon from '../../../components/SvgIcon';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showNotification } from '../../../store/notification.slice';
|
||||
import { updateKnowledgeBase, deleteKnowledgeBase } from '../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
|
||||
// 导入拆分的组件
|
||||
import Breadcrumb from './components/Breadcrumb';
|
||||
import KnowledgeBaseForm from './components/KnowledgeBaseForm';
|
||||
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
||||
|
||||
export default function SettingsTab({ knowledgeBase }) {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// State for knowledge base form
|
||||
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({
|
||||
name: knowledgeBase.name,
|
||||
desc: knowledgeBase.desc,
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
// State for pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const usersPerPage = 10;
|
||||
@ -13,7 +29,6 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
// State for edit modal
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editUser, setEditUser] = useState(null);
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
|
||||
// Mock data for users with permissions - convert to state so we can update it
|
||||
const [users, setUsers] = useState([
|
||||
@ -109,17 +124,108 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser);
|
||||
const totalPages = Math.ceil(users.length / usersPerPage);
|
||||
|
||||
// Handle knowledge base form input change
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setKnowledgeBaseForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
// Clear error if exists
|
||||
if (formErrors[name]) {
|
||||
setFormErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: '',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Validate knowledge base form
|
||||
const validateForm = () => {
|
||||
const errors = {};
|
||||
|
||||
if (!knowledgeBaseForm.name.trim()) {
|
||||
errors.name = '请输入知识库名称';
|
||||
}
|
||||
|
||||
if (!knowledgeBaseForm.desc.trim()) {
|
||||
errors.desc = '请输入知识库描述';
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// Here you would typically call an API to update the knowledge base settings
|
||||
console.log('Updating knowledge base settings');
|
||||
|
||||
// Validate form
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Dispatch update knowledge base action
|
||||
dispatch(
|
||||
updateKnowledgeBase({
|
||||
id: knowledgeBase.id,
|
||||
data: {
|
||||
name: knowledgeBaseForm.name,
|
||||
desc: knowledgeBaseForm.desc,
|
||||
},
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '知识库更新成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `更新失败: ${error.message || '未知错误'}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle knowledge base deletion
|
||||
const handleDelete = () => {
|
||||
// Here you would typically call an API to delete the knowledge base
|
||||
console.log('Deleting knowledge base:', knowledgeBase.id);
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Dispatch delete knowledge base action
|
||||
dispatch(deleteKnowledgeBase(knowledgeBase.id))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '知识库已删除',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
// Navigate back to knowledge base list
|
||||
navigate('/knowledge-base');
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `删除失败: ${error.message || '未知错误'}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
setIsSubmitting(false);
|
||||
setShowDeleteConfirm(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle edit user permissions
|
||||
@ -202,206 +308,27 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
return (
|
||||
<>
|
||||
{/* Breadcrumb navigation */}
|
||||
<div className='d-flex align-items-center mb-4 mt-3'>
|
||||
<nav aria-label='breadcrumb'>
|
||||
<ol className='breadcrumb mb-0'>
|
||||
<li className='breadcrumb-item'>
|
||||
<Link className='text-secondary text-decoration-none' to='/'>
|
||||
知识库
|
||||
</Link>
|
||||
</li>
|
||||
<li className='breadcrumb-item'>
|
||||
<Link
|
||||
className='text-secondary text-decoration-none'
|
||||
to={`/knowledge-base/${knowledgeBase.id}`}
|
||||
>
|
||||
{knowledgeBase.title}
|
||||
</Link>
|
||||
</li>
|
||||
<li className='breadcrumb-item active text-dark' aria-current='page'>
|
||||
设置
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<Breadcrumb knowledgeBase={knowledgeBase} activeTab='settings' />
|
||||
|
||||
{/* Settings form */}
|
||||
<div className='card border-0 shadow-sm'>
|
||||
<div className='card-body'>
|
||||
<h5 className='card-title mb-4'>知识库设置</h5>
|
||||
{/* Knowledge Base Form */}
|
||||
<KnowledgeBaseForm
|
||||
formData={knowledgeBaseForm}
|
||||
formErrors={formErrors}
|
||||
isSubmitting={isSubmitting}
|
||||
knowledgeBase={knowledgeBase}
|
||||
onInputChange={handleInputChange}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={() => setShowDeleteConfirm(true)}
|
||||
/>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='knowledgeTitle' className='form-label'>
|
||||
知识库名称
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='knowledgeTitle'
|
||||
defaultValue={knowledgeBase.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='knowledgeDescription' className='form-label'>
|
||||
知识库描述
|
||||
</label>
|
||||
<textarea
|
||||
className='form-control'
|
||||
id='knowledgeDescription'
|
||||
rows='3'
|
||||
defaultValue={knowledgeBase.description}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>访问权限</label>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='accessPermission'
|
||||
id='accessPublic'
|
||||
defaultChecked
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='accessPublic'>
|
||||
公开 - 所有人可访问
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='accessPermission'
|
||||
id='accessPrivate'
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='accessPrivate'>
|
||||
私有 - 仅创建者可访问
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>权限管理</label>
|
||||
<div className='card'>
|
||||
<div className='card-body p-0'>
|
||||
<table className='table mb-0'>
|
||||
<thead className='table-light'>
|
||||
<tr>
|
||||
<th scope='col'>ID</th>
|
||||
<th scope='col'>用户名</th>
|
||||
<th scope='col'>邮箱</th>
|
||||
<th scope='col'>权限类型</th>
|
||||
<th scope='col'>访问时长</th>
|
||||
<th scope='col'>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentUsers.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>#{user.id}</td>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<span
|
||||
className={
|
||||
user.permissionType === '完全访问'
|
||||
? 'text-success'
|
||||
: 'text-warning'
|
||||
}
|
||||
>
|
||||
{user.permissionType}
|
||||
</span>
|
||||
</td>
|
||||
<td>{user.accessDuration}</td>
|
||||
<td>
|
||||
<div className='d-flex gap-2'>
|
||||
<button
|
||||
className='btn btn-sm text-primary'
|
||||
title='编辑'
|
||||
onClick={() => handleEditUser(user)}
|
||||
>
|
||||
<SvgIcon className='edit' width='16' height='16' />
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-sm text-danger'
|
||||
title='删除'
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
>
|
||||
<SvgIcon className='trash' width='16' height='16' />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{users.length > usersPerPage && (
|
||||
<div className='d-flex justify-content-end mt-3'>
|
||||
<nav aria-label='Page navigation'>
|
||||
<ul className='pagination pagination-sm mb-0'>
|
||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||
<button
|
||||
className='page-link'
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<span aria-hidden='true'>«</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNumber) => (
|
||||
<li
|
||||
key={pageNumber}
|
||||
className={`page-item ${
|
||||
currentPage === pageNumber ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
className='page-link'
|
||||
onClick={() => handlePageChange(pageNumber)}
|
||||
>
|
||||
{pageNumber}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||
<button
|
||||
className='page-link'
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<span aria-hidden='true'>»</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button type='button' className='btn btn-outline-dark mt-2'>
|
||||
<SvgIcon className='plus' width='16' height='16' />
|
||||
<span className='ms-1'>添加用户</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='d-flex justify-content-end gap-2 mt-4'>
|
||||
<button type='button' className='btn btn-outline-danger' onClick={handleDelete}>
|
||||
删除知识库
|
||||
</button>
|
||||
<button type='submit' className='btn btn-dark'>
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/* Delete confirmation modal */}
|
||||
<DeleteConfirmModal
|
||||
show={showDeleteConfirm}
|
||||
title={knowledgeBase.name}
|
||||
isSubmitting={isSubmitting}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
|
||||
{/* Edit User Permissions Modal */}
|
||||
{showEditModal && (
|
||||
|
34
src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx
Normal file
34
src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* 面包屑导航组件
|
||||
*/
|
||||
const Breadcrumb = ({ knowledgeBase, activeTab }) => {
|
||||
return (
|
||||
<div className='d-flex align-items-center mb-4 mt-3'>
|
||||
<nav aria-label='breadcrumb'>
|
||||
<ol className='breadcrumb mb-0'>
|
||||
<li className='breadcrumb-item'>
|
||||
<Link className='text-secondary text-decoration-none' to='/knowledge-base'>
|
||||
知识库
|
||||
</Link>
|
||||
</li>
|
||||
<li className='breadcrumb-item'>
|
||||
<Link
|
||||
className='text-secondary text-decoration-none'
|
||||
to={`/knowledge-base/${knowledgeBase.id}`}
|
||||
>
|
||||
{knowledgeBase.name}
|
||||
</Link>
|
||||
</li>
|
||||
<li className='breadcrumb-item active text-dark' aria-current='page'>
|
||||
{activeTab === 'datasets' ? '数据集' : '设置'}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumb;
|
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* 删除确认模态框组件
|
||||
*/
|
||||
const DeleteConfirmModal = ({ show, title, isSubmitting, onCancel, onConfirm }) => {
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<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: '400px',
|
||||
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={onCancel}
|
||||
disabled={isSubmitting}
|
||||
aria-label='Close'
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<p>您确定要删除知识库 "{title}" 吗?此操作不可撤销。</p>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onCancel} disabled={isSubmitting}>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-danger' onClick={onConfirm} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
删除中...
|
||||
</>
|
||||
) : (
|
||||
'确认删除'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConfirmModal;
|
72
src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
Normal file
72
src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '../../../../components/SvgIcon';
|
||||
|
||||
/**
|
||||
* 文档列表组件
|
||||
*/
|
||||
const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocument, onDeleteDocument, selectAll }) => {
|
||||
if (documents.length === 0) {
|
||||
return <div className='alert alert-warning'>暂无数据集,请上传数据集</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='table-responsive'>
|
||||
<table className='table table-hover'>
|
||||
<thead className='table-light'>
|
||||
<tr>
|
||||
<th scope='col' width='40'>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
checked={selectAll}
|
||||
onChange={onSelectAll}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th scope='col'>名称</th>
|
||||
<th scope='col'>描述</th>
|
||||
<th scope='col'>大小</th>
|
||||
<th scope='col'>更新时间</th>
|
||||
<th scope='col' width='100'>
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((doc) => (
|
||||
<tr key={doc.id}>
|
||||
<td>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
checked={selectedDocuments.includes(doc.id)}
|
||||
onChange={() => onSelectDocument(doc.id)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>{doc.name}</td>
|
||||
<td>{doc.description}</td>
|
||||
<td>{doc.size}</td>
|
||||
<td>{new Date(doc.update_time).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<div className='d-flex gap-1'>
|
||||
<button
|
||||
className='btn btn-sm text-danger'
|
||||
title='删除'
|
||||
onClick={() => onDeleteDocument(doc.id)}
|
||||
>
|
||||
<SvgIcon className='trash' width='16' height='16' />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentList;
|
118
src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
Normal file
118
src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
/**
|
||||
* 文件上传模态框组件
|
||||
*/
|
||||
const FileUploadModal = ({
|
||||
show,
|
||||
newFile,
|
||||
fileErrors,
|
||||
isSubmitting,
|
||||
onClose,
|
||||
onDescriptionChange,
|
||||
onFileChange,
|
||||
onFileDrop,
|
||||
onDragOver,
|
||||
onUploadAreaClick,
|
||||
onUpload,
|
||||
}) => {
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<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={onClose} aria-label='Close'></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div
|
||||
className={`mb-3 p-4 border rounded text-center ${
|
||||
fileErrors.file ? 'border-danger' : 'border-dashed'
|
||||
}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={onUploadAreaClick}
|
||||
onDrop={onFileDrop}
|
||||
onDragOver={onDragOver}
|
||||
>
|
||||
<input type='file' ref={fileInputRef} className='d-none' onChange={onFileChange} />
|
||||
{newFile.file ? (
|
||||
<div>
|
||||
<p className='mb-1'>已选择文件:</p>
|
||||
<p className='fw-bold mb-0'>{newFile.file.name}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
||||
<p className='text-muted small mb-0'>支持 PDF, DOCX, TXT, CSV 等格式</p>
|
||||
</div>
|
||||
)}
|
||||
{fileErrors.file && <div className='text-danger mt-2'>{fileErrors.file}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='fileDescription' className='form-label'>
|
||||
文件描述
|
||||
</label>
|
||||
<textarea
|
||||
className='form-control'
|
||||
id='fileDescription'
|
||||
rows='3'
|
||||
value={newFile.description}
|
||||
onChange={onDescriptionChange}
|
||||
placeholder='请输入文件描述(可选)'
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={onUpload}
|
||||
disabled={!newFile.file || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
'上传'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploadModal;
|
116
src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx
Normal file
116
src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx
Normal file
@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* 知识库表单组件
|
||||
*/
|
||||
const KnowledgeBaseForm = ({
|
||||
formData,
|
||||
formErrors,
|
||||
isSubmitting,
|
||||
knowledgeBase,
|
||||
onInputChange,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div className='card border-0 shadow-sm'>
|
||||
<div className='card-body'>
|
||||
<h5 className='card-title mb-4'>知识库设置</h5>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='name' className='form-label'>
|
||||
知识库名称 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control ${formErrors.name ? 'is-invalid' : ''}`}
|
||||
id='name'
|
||||
name='name'
|
||||
value={formData.name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
{formErrors.name && <div className='invalid-feedback'>{formErrors.name}</div>}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='desc' className='form-label'>
|
||||
知识库描述 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${formErrors.desc ? 'is-invalid' : ''}`}
|
||||
id='desc'
|
||||
name='desc'
|
||||
rows='3'
|
||||
value={formData.desc}
|
||||
onChange={onInputChange}
|
||||
></textarea>
|
||||
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>知识库类型</label>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='knowledgeType'
|
||||
id='typePrivate'
|
||||
checked={knowledgeBase.type === 'private'}
|
||||
readOnly
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='typePrivate'>
|
||||
私有知识库
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='knowledgeType'
|
||||
id='typePublic'
|
||||
checked={knowledgeBase.type === 'public'}
|
||||
readOnly
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='typePublic'>
|
||||
公共知识库
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>创建时间</label>
|
||||
<p className='form-control-static'>{new Date(knowledgeBase.create_time).toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>最后更新时间</label>
|
||||
<p className='form-control-static'>{new Date(knowledgeBase.update_time).toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div className='d-flex justify-content-between'>
|
||||
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
'保存设置'
|
||||
)}
|
||||
</button>
|
||||
<button type='button' className='btn btn-danger' onClick={onDelete} disabled={isSubmitting}>
|
||||
删除知识库
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseForm;
|
@ -1,10 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import KnowledgeCard from './KnowledgeCard';
|
||||
import { useDispatch } from 'react-redux';
|
||||
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();
|
||||
@ -21,36 +32,146 @@ export default function KnowledgeBase() {
|
||||
reason: '',
|
||||
});
|
||||
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
});
|
||||
|
||||
const knowledgeList = [
|
||||
{
|
||||
id: '1',
|
||||
title: '产品开发知识库',
|
||||
description: '产品开发流程及规范说明文档',
|
||||
documents: 24,
|
||||
date: '2025-02-15',
|
||||
access: 'full',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '市场分析知识库',
|
||||
description: '2025年Q1市场分析总结',
|
||||
documents: 12,
|
||||
date: '2025-02-10',
|
||||
access: 'read',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '财务知识库',
|
||||
description: '月度财务分析报告',
|
||||
documents: 8,
|
||||
date: '2025-02-01',
|
||||
access: 'none',
|
||||
},
|
||||
];
|
||||
// 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;
|
||||
@ -87,12 +208,12 @@ export default function KnowledgeBase() {
|
||||
const validateCreateForm = () => {
|
||||
const errors = {};
|
||||
|
||||
if (!newKnowledgeBase.title.trim()) {
|
||||
errors.title = '请输入知识库名称';
|
||||
if (!newKnowledgeBase.name.trim()) {
|
||||
errors.name = '请输入知识库名称';
|
||||
}
|
||||
|
||||
if (!newKnowledgeBase.description.trim()) {
|
||||
errors.description = '请输入知识库描述';
|
||||
if (!newKnowledgeBase.desc.trim()) {
|
||||
errors.desc = '请输入知识库描述';
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
@ -120,27 +241,22 @@ export default function KnowledgeBase() {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, just show a success notification
|
||||
// Dispatch create knowledge base action
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '知识库创建成功',
|
||||
type: 'success',
|
||||
createKnowledgeBase({
|
||||
name: newKnowledgeBase.name,
|
||||
desc: newKnowledgeBase.desc,
|
||||
type: 'private', // Default type
|
||||
})
|
||||
);
|
||||
|
||||
// In a real application, you would get the ID from the API response
|
||||
// For now, we'll generate a mock ID
|
||||
const newId = Date.now().toString();
|
||||
|
||||
// Reset form and close modal
|
||||
setNewKnowledgeBase({ title: '', description: '' });
|
||||
setNewKnowledgeBase({ name: '', desc: '' });
|
||||
setFormErrors({});
|
||||
setShowCreateModal(false);
|
||||
|
||||
// Navigate to the newly created knowledge base with datasets tab
|
||||
navigate(`/knowledge-base/${newId}/datasets`);
|
||||
};
|
||||
|
||||
// Handle card click to navigate to knowledge base detail
|
||||
const handleCardClick = (id) => {
|
||||
navigate(`/knowledge-base/${id}/datasets`);
|
||||
};
|
||||
@ -179,10 +295,19 @@ export default function KnowledgeBase() {
|
||||
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'>
|
||||
<input type='text' className='form-control w-50' placeholder='搜索知识库...' />
|
||||
<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)}
|
||||
@ -192,105 +317,52 @@ export default function KnowledgeBase() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='row gap-3 m-0'>
|
||||
{knowledgeList.map((item) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<KnowledgeCard
|
||||
{...item}
|
||||
onClick={() => handleCardClick(item.id)}
|
||||
onRequestAccess={handleRequestAccess}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 新建知识库弹窗 */}
|
||||
{showCreateModal && (
|
||||
<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={() => setShowCreateModal(false)}
|
||||
aria-label='Close'
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='knowledgeTitle' className='form-label'>
|
||||
知识库名称 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control ${formErrors.title ? 'is-invalid' : ''}`}
|
||||
id='knowledgeTitle'
|
||||
name='title'
|
||||
value={newKnowledgeBase.title}
|
||||
onChange={handleInputChange}
|
||||
placeholder='请输入知识库名称'
|
||||
required
|
||||
/>
|
||||
{formErrors.title && <div className='invalid-feedback'>{formErrors.title}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='knowledgeDescription' className='form-label'>
|
||||
知识库描述 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${formErrors.description ? 'is-invalid' : ''}`}
|
||||
id='knowledgeDescription'
|
||||
name='description'
|
||||
value={newKnowledgeBase.description}
|
||||
onChange={handleInputChange}
|
||||
placeholder='请输入知识库描述'
|
||||
rows='3'
|
||||
required
|
||||
></textarea>
|
||||
{formErrors.description && (
|
||||
<div className='invalid-feedback'>{formErrors.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer d-flex justify-content-end gap-2'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-outline-secondary'
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark' onClick={handleCreateKnowledgeBase}>
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</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'
|
||||
@ -325,54 +397,29 @@ export default function KnowledgeBase() {
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-4'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='key' />
|
||||
访问级别 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<div className='d-flex gap-2'>
|
||||
<div
|
||||
className={`p-3 rounded border bg-warning-subtle ${
|
||||
accessRequestData.accessType === '只读访问'
|
||||
? 'border-warning bg-warning-subtle'
|
||||
: 'border-warning-subtle opacity-50'
|
||||
}`}
|
||||
style={{ flex: 1, cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' }))
|
||||
}
|
||||
>
|
||||
<div className='text-center text-warning fw-bold mb-1'>只读访问</div>
|
||||
<div className='text-center text-muted small'>仅查看数据集内容</div>
|
||||
</div>
|
||||
<div
|
||||
className={`p-3 rounded border bg-success-subtle ${
|
||||
accessRequestData.accessType === '完全访问'
|
||||
? 'border-success'
|
||||
: 'border-success-subtle opacity-50'
|
||||
}`}
|
||||
style={{ flex: 1, cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' }))
|
||||
}
|
||||
>
|
||||
<div className='text-center text-success fw-bold mb-1'>完全访问</div>
|
||||
<div className='text-center text-muted small'>查看、编辑和管理数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='calendar' />
|
||||
访问时长 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<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}
|
||||
required
|
||||
>
|
||||
<option value='一周'>一周</option>
|
||||
<option value='一个月'>一个月</option>
|
||||
@ -381,54 +428,50 @@ export default function KnowledgeBase() {
|
||||
<option value='永久'>永久</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='clipboard' />
|
||||
<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='请输入项目信息'
|
||||
required
|
||||
/>
|
||||
{accessRequestErrors.projectInfo && (
|
||||
<div className='invalid-feedback'>{accessRequestErrors.projectInfo}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='chat' />
|
||||
<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='请输入申请原因'
|
||||
rows='4'
|
||||
required
|
||||
></textarea>
|
||||
{accessRequestErrors.reason && (
|
||||
<div className='invalid-feedback'>{accessRequestErrors.reason}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer d-flex justify-content-end gap-2'>
|
||||
<div className='modal-footer'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-outline-secondary'
|
||||
className='btn btn-secondary'
|
||||
onClick={() => setShowAccessRequestModal(false)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark' onClick={handleSubmitAccessRequest}>
|
||||
<button type='button' className='btn btn-primary' onClick={handleSubmitAccessRequest}>
|
||||
提交申请
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
import DatasetTab from './Detail/DatasetTab';
|
||||
import SettingsTab from './Detail/SettingsTab';
|
||||
@ -7,8 +9,13 @@ import SettingsTab from './Detail/SettingsTab';
|
||||
export default function KnowledgeBaseDetail() {
|
||||
const { id, tab } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
|
||||
|
||||
// Get knowledge base details from Redux store
|
||||
const { items: knowledgeBases } = useSelector((state) => state.knowledgeBase.list);
|
||||
const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
|
||||
|
||||
// Update active tab when URL changes
|
||||
useEffect(() => {
|
||||
if (tab) {
|
||||
@ -16,15 +23,18 @@ export default function KnowledgeBaseDetail() {
|
||||
}
|
||||
}, [tab]);
|
||||
|
||||
// Mock data for the knowledge base details
|
||||
const knowledgeBase = {
|
||||
id: id,
|
||||
title: '知识库 1',
|
||||
description: '知识库详细信息',
|
||||
createdAt: '2023-05-01',
|
||||
updatedAt: '2023-05-15',
|
||||
documentsCount: 24,
|
||||
};
|
||||
// If knowledge base not found in Redux store, show notification and redirect
|
||||
useEffect(() => {
|
||||
if (!knowledgeBase && knowledgeBases.length > 0) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '未找到知识库,请返回知识库列表',
|
||||
type: 'warning',
|
||||
})
|
||||
);
|
||||
navigate('/knowledge-base');
|
||||
}
|
||||
}, [knowledgeBase, knowledgeBases, dispatch, navigate]);
|
||||
|
||||
// Handle tab change
|
||||
const handleTabChange = (tab) => {
|
||||
@ -32,14 +42,25 @@ export default function KnowledgeBaseDetail() {
|
||||
navigate(`/knowledge-base/${id}/${tab}`);
|
||||
};
|
||||
|
||||
// Show loading state if knowledge base not loaded yet
|
||||
if (!knowledgeBase) {
|
||||
return (
|
||||
<div className='container-fluid px-4 py-5 text-center'>
|
||||
<div className='spinner-border' role='status'>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='container-fluid px-4'>
|
||||
<div className='row'>
|
||||
{/* Sidebar */}
|
||||
<div className='col-md-3 col-lg-2 border-end'>
|
||||
<div className='py-4'>
|
||||
<div className='h4 mb-3 text-center'>{knowledgeBase.title}</div>
|
||||
<p className='text-center text-muted small mb-4'>{knowledgeBase.description}</p>
|
||||
<div className='h4 mb-3 text-center'>{knowledgeBase.name}</div>
|
||||
<p className='text-center text-muted small mb-4'>{knowledgeBase.desc}</p>
|
||||
|
||||
<hr />
|
||||
|
||||
|
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '../../../components/SvgIcon';
|
||||
|
||||
/**
|
||||
* 创建知识库模态框组件
|
||||
*/
|
||||
const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => {
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<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={onClose} aria-label='Close'></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='name' className='form-label'>
|
||||
知识库名称 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control ${formErrors.name ? 'is-invalid' : ''}`}
|
||||
id='name'
|
||||
name='name'
|
||||
value={formData.name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{formErrors.name && <div className='invalid-feedback'>{formErrors.name}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='desc' className='form-label'>
|
||||
知识库描述 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${formErrors.desc ? 'is-invalid' : ''}`}
|
||||
id='desc'
|
||||
name='desc'
|
||||
rows='3'
|
||||
value={formData.desc}
|
||||
onChange={onChange}
|
||||
></textarea>
|
||||
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-primary' onClick={onSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
'创建'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateKnowledgeBaseModal;
|
36
src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx
Normal file
36
src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import KnowledgeCard from '../KnowledgeCard';
|
||||
|
||||
/**
|
||||
* 知识库列表组件
|
||||
*/
|
||||
const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess }) => {
|
||||
if (knowledgeBases.length === 0) {
|
||||
return (
|
||||
<div className='alert alert-warning'>
|
||||
{isSearching ? '没有找到匹配的知识库' : '暂无知识库,请创建新的知识库'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='row gap-3 m-0'>
|
||||
{knowledgeBases.map((item) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<KnowledgeCard
|
||||
id={item.id}
|
||||
title={item.name}
|
||||
description={item.desc}
|
||||
documents={item.document_count || 0}
|
||||
date={new Date(item.create_time).toLocaleDateString()}
|
||||
access={item.permissions.can_edit ? 'full' : item.permissions.can_read ? 'read' : 'none'}
|
||||
onClick={() => onCardClick(item.id)}
|
||||
onRequestAccess={onRequestAccess}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseList;
|
53
src/pages/KnowledgeBase/components/Pagination.jsx
Normal file
53
src/pages/KnowledgeBase/components/Pagination.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* 分页组件
|
||||
*/
|
||||
const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => {
|
||||
return (
|
||||
<div className='d-flex justify-content-between align-items-center mt-4'>
|
||||
<div>
|
||||
<select
|
||||
className='form-select'
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
||||
>
|
||||
<option value='10'>10条/页</option>
|
||||
<option value='20'>20条/页</option>
|
||||
<option value='50'>50条/页</option>
|
||||
</select>
|
||||
</div>
|
||||
<nav aria-label='知识库分页'>
|
||||
<ul className='pagination mb-0'>
|
||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||
<button
|
||||
className='page-link'
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
</li>
|
||||
{[...Array(totalPages).keys()].map((i) => (
|
||||
<li key={i} className={`page-item ${currentPage === i + 1 ? 'active' : ''}`}>
|
||||
<button className='page-link' onClick={() => onPageChange(i + 1)}>
|
||||
{i + 1}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||
<button
|
||||
className='page-link'
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
29
src/pages/KnowledgeBase/components/SearchBar.jsx
Normal file
29
src/pages/KnowledgeBase/components/SearchBar.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '../../../components/SvgIcon';
|
||||
|
||||
/**
|
||||
* 知识库搜索栏组件
|
||||
*/
|
||||
const SearchBar = ({ searchKeyword, isSearching, onSearchChange, onSearch, onClearSearch }) => {
|
||||
return (
|
||||
<form className='d-flex w-50' onSubmit={onSearch}>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
placeholder='搜索知识库...'
|
||||
value={searchKeyword}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
{/* <button type='submit' className='btn btn-outline-dark ms-2'>
|
||||
<SvgIcon className={'search'} />
|
||||
</button> */}
|
||||
{isSearching && (
|
||||
<button type='button' className='btn btn-outline-dark ms-2' onClick={onClearSearch}>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
@ -34,7 +34,7 @@ api.interceptors.response.use(
|
||||
// Handle errors in the response
|
||||
if (error.response) {
|
||||
// monitor /verify
|
||||
if (error.response.status === 401 && error.config.url === '/check-token/') {
|
||||
if (error.response.status === 401 && error.config.url === '/auth/verify-token/') {
|
||||
if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
@ -10,15 +10,16 @@ export const loginThunk = createAsyncThunk(
|
||||
'auth/login',
|
||||
async ({ username, password }, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { message, user, token } = await post('/login/', { username, password });
|
||||
if (!user) {
|
||||
const { message, data } = await post('/auth/login/', { username, password });
|
||||
if (!data) {
|
||||
throw new Error(message || 'Something went wrong');
|
||||
}
|
||||
const { token } = data;
|
||||
// encrypt token
|
||||
const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
|
||||
sessionStorage.setItem('token', encryptedToken);
|
||||
|
||||
return user;
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Something went wrong';
|
||||
dispatch(
|
||||
@ -51,9 +52,9 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje
|
||||
}
|
||||
});
|
||||
|
||||
export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectWithValue, dispatch }) => {
|
||||
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { user, message } = await get('/check-token/');
|
||||
const { user, message } = await get('/auth/verify-token/');
|
||||
if (!user) {
|
||||
dispatch(logout());
|
||||
throw new Error(message || 'No token found');
|
||||
@ -69,7 +70,7 @@ export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectW
|
||||
export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
// Send the logout request to the server (this assumes your server clears any session-related info)
|
||||
await post('/logout/');
|
||||
await post('/auth/logout/');
|
||||
dispatch(logout());
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Log out failed';
|
||||
|
198
src/store/knowledgeBase/knowledgeBase.slice.js
Normal file
198
src/store/knowledgeBase/knowledgeBase.slice.js
Normal file
@ -0,0 +1,198 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
fetchKnowledgeBases,
|
||||
searchKnowledgeBases,
|
||||
createKnowledgeBase,
|
||||
getKnowledgeBaseById,
|
||||
updateKnowledgeBase,
|
||||
deleteKnowledgeBase,
|
||||
} from './knowledgeBase.thunks';
|
||||
|
||||
const initialState = {
|
||||
// List state
|
||||
list: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// Search state
|
||||
search: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
keyword: '',
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// Current knowledge base details
|
||||
current: {
|
||||
data: null,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// Create/update/delete operations status
|
||||
operations: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
operationType: null, // 'create' | 'update' | 'delete'
|
||||
},
|
||||
};
|
||||
|
||||
const knowledgeBaseSlice = createSlice({
|
||||
name: 'knowledgeBase',
|
||||
initialState,
|
||||
reducers: {
|
||||
resetOperationStatus: (state) => {
|
||||
state.operations = {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
operationType: null,
|
||||
};
|
||||
},
|
||||
resetCurrentKnowledgeBase: (state) => {
|
||||
state.current = {
|
||||
data: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
resetSearchState: (state) => {
|
||||
state.search = {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
keyword: '',
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// Fetch knowledge bases
|
||||
.addCase(fetchKnowledgeBases.pending, (state) => {
|
||||
state.list.status = 'loading';
|
||||
})
|
||||
.addCase(fetchKnowledgeBases.fulfilled, (state, action) => {
|
||||
state.list.status = 'succeeded';
|
||||
state.list.items = action.payload.items;
|
||||
state.list.total = action.payload.total;
|
||||
state.list.page = action.payload.page;
|
||||
state.list.page_size = action.payload.page_size;
|
||||
state.list.error = null;
|
||||
})
|
||||
.addCase(fetchKnowledgeBases.rejected, (state, action) => {
|
||||
state.list.status = 'failed';
|
||||
state.list.error = action.payload;
|
||||
})
|
||||
|
||||
// Search knowledge bases
|
||||
.addCase(searchKnowledgeBases.pending, (state, action) => {
|
||||
state.search.status = 'loading';
|
||||
// Store the keyword for reference
|
||||
if (action.meta.arg.keyword) {
|
||||
state.search.keyword = action.meta.arg.keyword;
|
||||
}
|
||||
})
|
||||
.addCase(searchKnowledgeBases.fulfilled, (state, action) => {
|
||||
state.search.status = 'succeeded';
|
||||
state.search.items = action.payload.items;
|
||||
state.search.total = action.payload.total;
|
||||
state.search.page = action.payload.page;
|
||||
state.search.page_size = action.payload.page_size;
|
||||
state.search.error = null;
|
||||
})
|
||||
.addCase(searchKnowledgeBases.rejected, (state, action) => {
|
||||
state.search.status = 'failed';
|
||||
state.search.error = action.payload;
|
||||
})
|
||||
|
||||
// Get knowledge base by ID
|
||||
.addCase(getKnowledgeBaseById.pending, (state) => {
|
||||
state.current.status = 'loading';
|
||||
})
|
||||
.addCase(getKnowledgeBaseById.fulfilled, (state, action) => {
|
||||
state.current.status = 'succeeded';
|
||||
state.current.data = action.payload;
|
||||
state.current.error = null;
|
||||
})
|
||||
.addCase(getKnowledgeBaseById.rejected, (state, action) => {
|
||||
state.current.status = 'failed';
|
||||
state.current.error = action.payload;
|
||||
})
|
||||
|
||||
// Create knowledge base
|
||||
.addCase(createKnowledgeBase.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
state.operations.operationType = 'create';
|
||||
})
|
||||
.addCase(createKnowledgeBase.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
// Don't add to list here - better to refetch the list to ensure consistency
|
||||
state.operations.error = null;
|
||||
})
|
||||
.addCase(createKnowledgeBase.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload;
|
||||
})
|
||||
|
||||
// Update knowledge base
|
||||
.addCase(updateKnowledgeBase.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
state.operations.operationType = 'update';
|
||||
})
|
||||
.addCase(updateKnowledgeBase.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
// Update in list if present
|
||||
const index = state.list.items.findIndex((item) => item.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.list.items[index] = action.payload;
|
||||
}
|
||||
// Update in search results if present
|
||||
const searchIndex = state.search.items.findIndex((item) => item.id === action.payload.id);
|
||||
if (searchIndex !== -1) {
|
||||
state.search.items[searchIndex] = action.payload;
|
||||
}
|
||||
// Update current if it's the same knowledge base
|
||||
if (state.current.data && state.current.data.id === action.payload.id) {
|
||||
state.current.data = action.payload;
|
||||
}
|
||||
state.operations.error = null;
|
||||
})
|
||||
.addCase(updateKnowledgeBase.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload;
|
||||
})
|
||||
|
||||
// Delete knowledge base
|
||||
.addCase(deleteKnowledgeBase.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
state.operations.operationType = 'delete';
|
||||
})
|
||||
.addCase(deleteKnowledgeBase.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
// Remove from list if present
|
||||
state.list.items = state.list.items.filter((item) => item.id !== action.payload);
|
||||
// Remove from search results if present
|
||||
state.search.items = state.search.items.filter((item) => item.id !== action.payload);
|
||||
// Reset current if it's the same knowledge base
|
||||
if (state.current.data && state.current.data.id === action.payload) {
|
||||
state.current.data = null;
|
||||
}
|
||||
state.operations.error = null;
|
||||
})
|
||||
.addCase(deleteKnowledgeBase.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { resetOperationStatus, resetCurrentKnowledgeBase, resetSearchState } = knowledgeBaseSlice.actions;
|
||||
const knowledgeBaseReducer = knowledgeBaseSlice.reducer;
|
||||
export default knowledgeBaseReducer;
|
107
src/store/knowledgeBase/knowledgeBase.thunks.js
Normal file
107
src/store/knowledgeBase/knowledgeBase.thunks.js
Normal file
@ -0,0 +1,107 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post, put, del } from '../../services/api';
|
||||
|
||||
/**
|
||||
* Fetch knowledge bases with pagination
|
||||
* @param {Object} params - Pagination parameters
|
||||
* @param {number} params.page - Page number (default: 1)
|
||||
* @param {number} params.page_size - Page size (default: 10)
|
||||
*/
|
||||
export const fetchKnowledgeBases = createAsyncThunk(
|
||||
'knowledgeBase/fetchKnowledgeBases',
|
||||
async ({ page = 1, page_size = 10 } = {}, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get('/knowledge-bases/', { page, page_size });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to fetch knowledge bases');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Search knowledge bases
|
||||
* @param {Object} params - Search parameters
|
||||
* @param {string} params.keyword - Search keyword
|
||||
* @param {number} params.page - Page number (default: 1)
|
||||
* @param {number} params.page_size - Page size (default: 10)
|
||||
*/
|
||||
export const searchKnowledgeBases = createAsyncThunk(
|
||||
'knowledgeBase/searchKnowledgeBases',
|
||||
async ({ keyword, page = 1, page_size = 10 }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get('/knowledge-bases/search/', {
|
||||
keyword,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to search knowledge bases');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new knowledge base
|
||||
*/
|
||||
export const createKnowledgeBase = createAsyncThunk(
|
||||
'knowledgeBase/createKnowledgeBase',
|
||||
async (knowledgeBaseData, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await post('/knowledge-bases/', knowledgeBaseData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to create knowledge base');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get knowledge base details by ID
|
||||
*/
|
||||
export const getKnowledgeBaseById = createAsyncThunk(
|
||||
'knowledgeBase/getKnowledgeBaseById',
|
||||
async (id, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get(`/knowledge-bases/${id}/`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to get knowledge base details');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Update knowledge base
|
||||
* @param {Object} params - Update parameters
|
||||
* @param {string} params.id - Knowledge base ID
|
||||
* @param {Object} params.data - Update data (name, desc)
|
||||
*/
|
||||
export const updateKnowledgeBase = createAsyncThunk(
|
||||
'knowledgeBase/updateKnowledgeBase',
|
||||
async ({ id, data }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await put(`/knowledge-bases/${id}/`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to update knowledge base');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete knowledge base
|
||||
* @param {string} id - Knowledge base ID
|
||||
*/
|
||||
export const deleteKnowledgeBase = createAsyncThunk(
|
||||
'knowledgeBase/deleteKnowledgeBase',
|
||||
async (id, { rejectWithValue }) => {
|
||||
try {
|
||||
await del(`/knowledge-bases/${id}/`);
|
||||
return id;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to delete knowledge base');
|
||||
}
|
||||
}
|
||||
);
|
@ -3,10 +3,12 @@ import { persistReducer, persistStore } from 'redux-persist';
|
||||
import sessionStorage from 'redux-persist/lib/storage/session';
|
||||
import notificationReducer from './notification.slice.js';
|
||||
import authReducer from './auth/auth.slice.js';
|
||||
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
||||
|
||||
const rootRducer = combineReducers({
|
||||
auth: authReducer,
|
||||
notification: notificationReducer,
|
||||
knowledgeBase: knowledgeBaseReducer,
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
|
Loading…
Reference in New Issue
Block a user