mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 07:08:14 +08:00
commit
a415013f61
@ -6,6 +6,7 @@ import {
|
|||||||
updateKnowledgeBase,
|
updateKnowledgeBase,
|
||||||
deleteKnowledgeBase,
|
deleteKnowledgeBase,
|
||||||
changeKnowledgeBaseType,
|
changeKnowledgeBaseType,
|
||||||
|
getKnowledgeBaseDocuments,
|
||||||
} from '../../../store/knowledgeBase/knowledgeBase.thunks';
|
} from '../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
|
|
||||||
// 导入拆分的组件
|
// 导入拆分的组件
|
||||||
@ -13,6 +14,8 @@ import Breadcrumb from './components/Breadcrumb';
|
|||||||
import KnowledgeBaseForm from './components/KnowledgeBaseForm';
|
import KnowledgeBaseForm from './components/KnowledgeBaseForm';
|
||||||
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
||||||
import UserPermissionsManager from './components/UserPermissionsManager';
|
import UserPermissionsManager from './components/UserPermissionsManager';
|
||||||
|
import FileUploadModal from './components/FileUploadModal';
|
||||||
|
import DocumentList from './components/DocumentList';
|
||||||
|
|
||||||
// 部门和组别的映射关系
|
// 部门和组别的映射关系
|
||||||
const departmentGroups = {
|
const departmentGroups = {
|
||||||
@ -47,6 +50,14 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [availableGroups, setAvailableGroups] = useState([]);
|
const [availableGroups, setAvailableGroups] = useState([]);
|
||||||
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
|
|
||||||
|
// 获取文档列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (knowledgeBase?.id) {
|
||||||
|
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBase.id }));
|
||||||
|
}
|
||||||
|
}, [dispatch, knowledgeBase?.id]);
|
||||||
|
|
||||||
// 当部门变化时,更新可选的组别
|
// 当部门变化时,更新可选的组别
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -334,6 +345,27 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
availableGroups={availableGroups}
|
availableGroups={availableGroups}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Document Management Section */}
|
||||||
|
<div className='card border-0 shadow-sm mt-4'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<div className='d-flex justify-content-between align-items-center mb-4'>
|
||||||
|
<h5 className='card-title m-0'>文档管理</h5>
|
||||||
|
<button className='btn btn-primary' onClick={() => setShowUploadModal(true)}>
|
||||||
|
上传文档
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DocumentList knowledgeBaseId={knowledgeBase.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload Modal */}
|
||||||
|
<FileUploadModal
|
||||||
|
show={showUploadModal}
|
||||||
|
onClose={() => setShowUploadModal(false)}
|
||||||
|
knowledgeBaseId={knowledgeBase.id}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* User Permissions Manager */}
|
{/* User Permissions Manager */}
|
||||||
{/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */}
|
{/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */}
|
||||||
|
|
||||||
|
@ -1,12 +1,42 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SvgIcon from '../../../../components/SvgIcon';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { formatDate } from '../../../../utils/dateUtils';
|
||||||
|
import { deleteKnowledgeBaseDocument } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档列表组件
|
* 知识库文档列表组件
|
||||||
*/
|
*/
|
||||||
const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocument, onDeleteDocument, selectAll }) => {
|
const DocumentList = ({ knowledgeBaseId }) => {
|
||||||
if (documents.length === 0) {
|
const dispatch = useDispatch();
|
||||||
return <div className='alert alert-warning'>暂无数据集,请上传数据集</div>;
|
const { items, loading, pagination } = useSelector((state) => state.knowledgeBase.documents);
|
||||||
|
|
||||||
|
const handleDeleteDocument = (documentId) => {
|
||||||
|
if (window.confirm('确定要删除此文档吗?')) {
|
||||||
|
dispatch(
|
||||||
|
deleteKnowledgeBaseDocument({
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
document_id: documentId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className='text-center py-4'>
|
||||||
|
<div className='spinner-border text-primary' role='status'>
|
||||||
|
<span className='visually-hidden'>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='text-center py-4 text-muted'>
|
||||||
|
<p>暂无文档,请上传文档</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -14,59 +44,51 @@ const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocum
|
|||||||
<table className='table table-hover'>
|
<table className='table table-hover'>
|
||||||
<thead className='table-light'>
|
<thead className='table-light'>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope='col' width='40'>
|
<th scope='col'>文档名称</th>
|
||||||
<div className='form-check'>
|
<th scope='col'>创建时间</th>
|
||||||
<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'>更新时间</th>
|
||||||
<th scope='col' width='100'>
|
<th scope='col'>操作</th>
|
||||||
操作
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{documents.map((doc) => (
|
{items.map((doc) => (
|
||||||
<tr key={doc.id}>
|
<tr key={doc.id}>
|
||||||
|
<td>{doc.document_name}</td>
|
||||||
|
<td>{formatDateTime(doc.create_time)}</td>
|
||||||
|
<td>{formatDateTime(doc.update_time)}</td>
|
||||||
<td>
|
<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
|
<button
|
||||||
className='btn btn-sm text-danger'
|
className='btn btn-sm btn-outline-danger'
|
||||||
title='删除'
|
onClick={() => handleDeleteDocument(doc.document_id)}
|
||||||
onClick={() => onDeleteDocument(doc.id)}
|
|
||||||
>
|
>
|
||||||
<SvgIcon className='trash' width='16' height='16' />
|
删除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{pagination.total > 0 && (
|
||||||
|
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||||
|
<p className='text-muted mb-0'>共 {pagination.total} 条记录</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to format date string
|
||||||
|
const formatDateTime = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
|
||||||
|
// If the utility function exists, use it, otherwise format manually
|
||||||
|
try {
|
||||||
|
return formatDate(dateString);
|
||||||
|
} catch (error) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default DocumentList;
|
export default DocumentList;
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { uploadDocument, getKnowledgeBaseDocuments } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件上传模态框组件
|
* 文件上传模态框组件
|
||||||
*/
|
*/
|
||||||
const FileUploadModal = ({
|
const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||||
show,
|
const dispatch = useDispatch();
|
||||||
newFile,
|
|
||||||
fileErrors,
|
|
||||||
isSubmitting,
|
|
||||||
onClose,
|
|
||||||
onDescriptionChange,
|
|
||||||
onFileChange,
|
|
||||||
onFileDrop,
|
|
||||||
onDragOver,
|
|
||||||
onUploadAreaClick,
|
|
||||||
onUpload,
|
|
||||||
}) => {
|
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const modalRef = useRef(null);
|
const modalRef = useRef(null);
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [fileError, setFileError] = useState('');
|
||||||
|
|
||||||
// 处理上传区域点击事件
|
// 处理上传区域点击事件
|
||||||
const handleUploadAreaClick = () => {
|
const handleUploadAreaClick = () => {
|
||||||
@ -28,13 +22,76 @@ const FileUploadModal = ({
|
|||||||
const handleDragOver = (e) => {
|
const handleDragOver = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDragOver?.(e);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e) => {
|
const handleDrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onFileDrop?.(e);
|
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
|
handleFileSelected(e.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
handleFileSelected(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelected = (file) => {
|
||||||
|
setFileError('');
|
||||||
|
setSelectedFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFileInput = () => {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setFileError('');
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
setFileError('请选择要上传的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(
|
||||||
|
uploadDocument({
|
||||||
|
knowledge_base_id: knowledgeBaseId,
|
||||||
|
file: selectedFile,
|
||||||
|
})
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// 成功上传后刷新文档列表
|
||||||
|
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
|
||||||
|
|
||||||
|
// Reset the file input
|
||||||
|
resetFileInput();
|
||||||
|
|
||||||
|
// 不要在上传成功后关闭模态框,允许用户继续上传或手动关闭
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error);
|
||||||
|
setFileError('文件上传失败: ' + (error?.message || '未知错误'));
|
||||||
|
|
||||||
|
// 清空选中的文件
|
||||||
|
resetFileInput();
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// 只有在非上传状态才允许关闭
|
||||||
|
if (!isUploading) {
|
||||||
|
resetFileInput();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
@ -78,64 +135,60 @@ const FileUploadModal = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
|
||||||
<h5 className='modal-title m-0'>上传文件</h5>
|
<h5 className='modal-title m-0'>上传文档</h5>
|
||||||
<button type='button' className='btn-close' onClick={onClose} aria-label='Close'></button>
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn-close'
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isUploading}
|
||||||
|
aria-label='Close'
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-body'>
|
<div className='modal-body'>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 p-4 border rounded text-center ${
|
className={`mb-3 p-4 border rounded text-center ${
|
||||||
fileErrors.file ? 'border-danger' : 'border-dashed'
|
fileError ? 'border-danger' : 'border-dashed'
|
||||||
}`}
|
}`}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: isUploading ? 'not-allowed' : 'pointer' }}
|
||||||
onClick={handleUploadAreaClick}
|
onClick={!isUploading ? handleUploadAreaClick : undefined}
|
||||||
onDrop={handleDrop}
|
onDrop={!isUploading ? handleDrop : undefined}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type='file'
|
type='file'
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className='d-none'
|
className='d-none'
|
||||||
onChange={onFileChange}
|
onChange={handleFileChange}
|
||||||
accept='.pdf,.docx,.txt,.csv'
|
accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
|
||||||
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
{newFile.file ? (
|
{selectedFile ? (
|
||||||
<div>
|
<div>
|
||||||
<p className='mb-1'>已选择文件:</p>
|
<p className='mb-1'>已选择文件:</p>
|
||||||
<p className='fw-bold mb-0'>{newFile.file.name}</p>
|
<p className='fw-bold mb-0'>{selectedFile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
<p className='mb-1'>点击或拖拽文件到此处上传</p>
|
||||||
<p className='text-muted small mb-0'>支持 PDF, DOCX, TXT, CSV 等格式</p>
|
<p className='text-muted small mb-0'>
|
||||||
|
支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{fileErrors.file && <div className='text-danger mt-2'>{fileErrors.file}</div>}
|
{fileError && <div className='text-danger mt-2'>{fileError}</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>
|
</div>
|
||||||
<div className='modal-footer gap-2'>
|
<div className='modal-footer gap-2'>
|
||||||
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
<button type='button' className='btn btn-secondary' onClick={handleClose} disabled={isUploading}>
|
||||||
取消
|
关闭
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-dark'
|
className='btn btn-primary'
|
||||||
onClick={onUpload}
|
onClick={handleUpload}
|
||||||
disabled={!newFile.file || isSubmitting}
|
disabled={!selectedFile || isUploading}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isUploading ? (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className='spinner-border spinner-border-sm me-2'
|
className='spinner-border spinner-border-sm me-2'
|
||||||
@ -145,7 +198,7 @@ const FileUploadModal = ({
|
|||||||
上传中...
|
上传中...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'上传'
|
'上传文档'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -522,8 +522,8 @@ const mockPermissionApi = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock API functions
|
// Mock API functions
|
||||||
export const mockGet = async (url, config = {}) => {
|
export const mockGet = async (url, params = {}) => {
|
||||||
console.log(`[MOCK API] GET ${url}`, config);
|
console.log(`[MOCK API] GET ${url}`, params);
|
||||||
|
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
@ -537,7 +537,6 @@ export const mockGet = async (url, config = {}) => {
|
|||||||
|
|
||||||
// Get knowledge bases
|
// Get knowledge bases
|
||||||
if (url === '/knowledge-bases/') {
|
if (url === '/knowledge-bases/') {
|
||||||
const params = config.params || { page: 1, page_size: 10 };
|
|
||||||
const result = paginate(knowledgeBases, params.page_size, params.page);
|
const result = paginate(knowledgeBases, params.page_size, params.page);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -576,7 +575,6 @@ export const mockGet = async (url, config = {}) => {
|
|||||||
|
|
||||||
// Get chat history
|
// Get chat history
|
||||||
if (url === '/chat-history/') {
|
if (url === '/chat-history/') {
|
||||||
const params = config.params || { page: 1, page_size: 10 };
|
|
||||||
const result = mockGetChatHistory(params);
|
const result = mockGetChatHistory(params);
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@ -620,7 +618,7 @@ export const mockGet = async (url, config = {}) => {
|
|||||||
|
|
||||||
// Knowledge base search
|
// Knowledge base search
|
||||||
if (url === '/knowledge-bases/search/') {
|
if (url === '/knowledge-bases/search/') {
|
||||||
const { keyword = '', page = 1, page_size = 10 } = config.params || {};
|
const { keyword = '', page = 1, page_size = 10 } = params.params || {};
|
||||||
const filtered = knowledgeBases.filter(
|
const filtered = knowledgeBases.filter(
|
||||||
(kb) =>
|
(kb) =>
|
||||||
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
@ -671,10 +669,58 @@ export const mockGet = async (url, config = {}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取知识库文档列表
|
||||||
|
if (url.match(/\/knowledge-bases\/([^/]+)\/documents\//)) {
|
||||||
|
const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/documents\//)[1];
|
||||||
|
const page = params?.params?.page || 1;
|
||||||
|
const page_size = params?.params?.page_size || 10;
|
||||||
|
|
||||||
|
// 模拟文档列表数据
|
||||||
|
const mockDocuments = [
|
||||||
|
{
|
||||||
|
id: 'df6d2c2b-895c-4c56-83c8-1644345e654d',
|
||||||
|
document_id: '772044ae-0ecf-11f0-8082-0242ac120002',
|
||||||
|
document_name: '产品说明书.pdf',
|
||||||
|
external_id: '772044ae-0ecf-11f0-8082-0242ac120002',
|
||||||
|
create_time: '2023-04-01 08:01:06',
|
||||||
|
update_time: '2023-04-01 08:01:06',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eba8f519-debf-461c-b4fd-87177d94bece',
|
||||||
|
document_id: '429a2c08-0ea3-11f0-bdec-0242ac120002',
|
||||||
|
document_name: '用户手册.docx',
|
||||||
|
external_id: '429a2c08-0ea3-11f0-bdec-0242ac120002',
|
||||||
|
create_time: '2023-04-01 02:44:38',
|
||||||
|
update_time: '2023-04-01 02:44:38',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7a9e4c31-5b2d-437e-9a8f-2b5c7e8a9d1e',
|
||||||
|
document_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e',
|
||||||
|
document_name: '技术文档.txt',
|
||||||
|
external_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e',
|
||||||
|
create_time: '2023-03-15 10:23:45',
|
||||||
|
update_time: '2023-03-15 10:23:45',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
code: 200,
|
||||||
|
message: '获取文档列表成功',
|
||||||
|
data: {
|
||||||
|
total: mockDocuments.length,
|
||||||
|
page: page,
|
||||||
|
page_size: page_size,
|
||||||
|
items: mockDocuments,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockPost = async (url, data) => {
|
export const mockPost = async (url, data, isMultipart = false) => {
|
||||||
console.log(`[MOCK API] POST ${url}`, data);
|
console.log(`[MOCK API] POST ${url}`, data);
|
||||||
|
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
@ -857,6 +903,26 @@ export const mockPost = async (url, data) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上传知识库文档
|
||||||
|
if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) {
|
||||||
|
const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1];
|
||||||
|
const file = isMultipart ? data.get('file') : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
code: 200,
|
||||||
|
message: 'Document uploaded successfully',
|
||||||
|
data: {
|
||||||
|
id: `doc-${Date.now()}`,
|
||||||
|
knowledge_base_id: knowledge_base_id,
|
||||||
|
filename: file ? file.name : 'mock-document.pdf',
|
||||||
|
status: 'processing',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -966,6 +1032,22 @@ export const mockDelete = async (url) => {
|
|||||||
return { data: mockDeleteChat(id) };
|
return { data: mockDeleteChat(id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除知识库文档
|
||||||
|
if (url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/)) {
|
||||||
|
const matches = url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/);
|
||||||
|
const knowledge_base_id = matches[1];
|
||||||
|
const document_id = matches[2];
|
||||||
|
|
||||||
|
console.log(`[MOCK API] Deleting document ${document_id} from knowledge base ${knowledge_base_id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
code: 200,
|
||||||
|
message: '文档删除成功',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,6 +8,9 @@ import {
|
|||||||
searchKnowledgeBases,
|
searchKnowledgeBases,
|
||||||
requestKnowledgeBaseAccess,
|
requestKnowledgeBaseAccess,
|
||||||
getKnowledgeBaseById,
|
getKnowledgeBaseById,
|
||||||
|
uploadDocument,
|
||||||
|
getKnowledgeBaseDocuments,
|
||||||
|
deleteKnowledgeBaseDocument,
|
||||||
} from './knowledgeBase.thunks';
|
} from './knowledgeBase.thunks';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
@ -27,6 +30,17 @@ const initialState = {
|
|||||||
batchLoading: false,
|
batchLoading: false,
|
||||||
editStatus: 'idle',
|
editStatus: 'idle',
|
||||||
requestAccessStatus: 'idle',
|
requestAccessStatus: 'idle',
|
||||||
|
uploadStatus: 'idle',
|
||||||
|
documents: {
|
||||||
|
items: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
pagination: {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const knowledgeBaseSlice = createSlice({
|
const knowledgeBaseSlice = createSlice({
|
||||||
@ -180,6 +194,57 @@ const knowledgeBaseSlice = createSlice({
|
|||||||
.addCase(getKnowledgeBaseById.rejected, (state, action) => {
|
.addCase(getKnowledgeBaseById.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.error = action.payload || 'Failed to get knowledge base details';
|
state.error = action.payload || 'Failed to get knowledge base details';
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传文档
|
||||||
|
.addCase(uploadDocument.pending, (state) => {
|
||||||
|
state.uploadStatus = 'loading';
|
||||||
|
})
|
||||||
|
.addCase(uploadDocument.fulfilled, (state) => {
|
||||||
|
state.uploadStatus = 'successful';
|
||||||
|
})
|
||||||
|
.addCase(uploadDocument.rejected, (state, action) => {
|
||||||
|
state.uploadStatus = 'failed';
|
||||||
|
state.error = action.payload || 'Failed to upload document';
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取知识库文档列表
|
||||||
|
.addCase(getKnowledgeBaseDocuments.pending, (state) => {
|
||||||
|
state.documents.loading = true;
|
||||||
|
state.documents.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getKnowledgeBaseDocuments.fulfilled, (state, action) => {
|
||||||
|
state.documents.loading = false;
|
||||||
|
state.documents.items = action.payload.items || [];
|
||||||
|
state.documents.pagination = {
|
||||||
|
total: action.payload.total || 0,
|
||||||
|
page: action.payload.page || 1,
|
||||||
|
page_size: action.payload.page_size || 10,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.addCase(getKnowledgeBaseDocuments.rejected, (state, action) => {
|
||||||
|
state.documents.loading = false;
|
||||||
|
state.documents.error = action.payload || 'Failed to get documents';
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除知识库文档
|
||||||
|
.addCase(deleteKnowledgeBaseDocument.pending, (state) => {
|
||||||
|
state.documents.loading = true;
|
||||||
|
state.documents.error = null;
|
||||||
|
})
|
||||||
|
.addCase(deleteKnowledgeBaseDocument.fulfilled, (state, action) => {
|
||||||
|
state.documents.loading = false;
|
||||||
|
const deletedDocId = action.payload;
|
||||||
|
state.documents.items = state.documents.items.filter(
|
||||||
|
(doc) => doc.document_id !== deletedDocId
|
||||||
|
);
|
||||||
|
if (state.documents.pagination.total > 0) {
|
||||||
|
state.documents.pagination.total -= 1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(deleteKnowledgeBaseDocument.rejected, (state, action) => {
|
||||||
|
state.documents.loading = false;
|
||||||
|
state.documents.error = action.payload || 'Failed to delete document';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { get, post, put, del } from '../../services/api';
|
import { get, post, put, del, upload } from '../../services/api';
|
||||||
import { showNotification } from '../notification.slice';
|
import { showNotification } from '../notification.slice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -196,3 +196,103 @@ export const requestKnowledgeBaseAccess = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a document to a knowledge base
|
||||||
|
* @param {Object} params - Upload parameters
|
||||||
|
* @param {string} params.knowledge_base_id - Knowledge base ID
|
||||||
|
* @param {File} params.file - File to upload
|
||||||
|
*/
|
||||||
|
export const uploadDocument = createAsyncThunk(
|
||||||
|
'knowledgeBase/uploadDocument',
|
||||||
|
async ({ knowledge_base_id, file }, { rejectWithValue, dispatch }) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await post(`/knowledge-bases/${knowledge_base_id}/upload_document/`, formData, true);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: `文档 ${file.name} 上传成功`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理新的返回格式
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || '文档上传失败';
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
type: 'danger',
|
||||||
|
message: errorMessage,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get documents list for a knowledge base
|
||||||
|
* @param {Object} params - Parameters
|
||||||
|
* @param {string} params.knowledge_base_id - Knowledge base ID
|
||||||
|
* @param {number} params.page - Page number (default: 1)
|
||||||
|
* @param {number} params.page_size - Page size (default: 10)
|
||||||
|
*/
|
||||||
|
export const getKnowledgeBaseDocuments = createAsyncThunk(
|
||||||
|
'knowledgeBase/getDocuments',
|
||||||
|
async ({ knowledge_base_id, page = 1, page_size = 10 }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await get(`/knowledge-bases/${knowledge_base_id}/documents/`, {
|
||||||
|
params: { page, page_size }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理返回格式
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error.response?.data?.message || '获取文档列表失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document from a knowledge base
|
||||||
|
* @param {Object} params - Parameters
|
||||||
|
* @param {string} params.knowledge_base_id - Knowledge base ID
|
||||||
|
* @param {string} params.document_id - Document ID
|
||||||
|
*/
|
||||||
|
export const deleteKnowledgeBaseDocument = createAsyncThunk(
|
||||||
|
'knowledgeBase/deleteDocument',
|
||||||
|
async ({ knowledge_base_id, document_id }, { rejectWithValue, dispatch }) => {
|
||||||
|
try {
|
||||||
|
await del(`/knowledge-bases/${knowledge_base_id}/documents/${document_id}/`);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: '文档删除成功',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return document_id;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
type: 'danger',
|
||||||
|
message: error.response?.data?.message || '文档删除失败',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return rejectWithValue(error.response?.data?.message || '文档删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
48
src/utils/dateUtils.js
Normal file
48
src/utils/dateUtils.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param {string} dateString - 日期字符串
|
||||||
|
* @returns {string} 格式化后的日期字符串
|
||||||
|
*/
|
||||||
|
export const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化为 YYYY-MM-DD HH:MM:SS
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期(仅日期部分)
|
||||||
|
* @param {string} dateString - 日期字符串
|
||||||
|
* @returns {string} 格式化后的日期字符串
|
||||||
|
*/
|
||||||
|
export const formatDateOnly = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
// 检查日期是否有效
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化为 YYYY-MM-DD
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user