mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 07:20:55 +08:00
[dev]add upload file api
This commit is contained in:
parent
d105e2a358
commit
ba53185050
@ -13,6 +13,7 @@ import Breadcrumb from './components/Breadcrumb';
|
||||
import KnowledgeBaseForm from './components/KnowledgeBaseForm';
|
||||
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
||||
import UserPermissionsManager from './components/UserPermissionsManager';
|
||||
import FileUploadModal from './components/FileUploadModal';
|
||||
|
||||
// 部门和组别的映射关系
|
||||
const departmentGroups = {
|
||||
@ -47,6 +48,7 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
|
||||
// 当部门变化时,更新可选的组别
|
||||
useEffect(() => {
|
||||
@ -93,7 +95,7 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
allowed = ['admin', 'member', 'private'].includes(value);
|
||||
} else {
|
||||
// 普通成员只能选择公共和private
|
||||
allowed = ['admin', 'private'].includes(value);
|
||||
allowed = ['admin', 'private'].includes(value);
|
||||
}
|
||||
|
||||
if (!allowed) {
|
||||
@ -334,6 +336,26 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
availableGroups={availableGroups}
|
||||
/>
|
||||
|
||||
{/* Document Upload Section */}
|
||||
<div className='card border-0 shadow-sm mt-4'>
|
||||
<div className='card-body'>
|
||||
<h5 className='card-title mb-4'>文档管理</h5>
|
||||
<p className='text-muted mb-3'>
|
||||
上传文档到知识库,支持PDF、Word、Excel、TXT、Markdown和CSV等格式。
|
||||
</p>
|
||||
<button className='btn btn-primary' onClick={() => setShowUploadModal(true)}>
|
||||
上传文档
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Upload Modal */}
|
||||
<FileUploadModal
|
||||
show={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
knowledgeBaseId={knowledgeBase.id}
|
||||
/>
|
||||
|
||||
{/* User Permissions Manager */}
|
||||
{/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */}
|
||||
|
||||
|
@ -1,23 +1,17 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { uploadDocument, getKnowledgeBaseById } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
|
||||
/**
|
||||
* 文件上传模态框组件
|
||||
*/
|
||||
const FileUploadModal = ({
|
||||
show,
|
||||
newFile,
|
||||
fileErrors,
|
||||
isSubmitting,
|
||||
onClose,
|
||||
onDescriptionChange,
|
||||
onFileChange,
|
||||
onFileDrop,
|
||||
onDragOver,
|
||||
onUploadAreaClick,
|
||||
onUpload,
|
||||
}) => {
|
||||
const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const fileInputRef = useRef(null);
|
||||
const modalRef = useRef(null);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [fileError, setFileError] = useState('');
|
||||
|
||||
// 处理上传区域点击事件
|
||||
const handleUploadAreaClick = () => {
|
||||
@ -28,13 +22,76 @@ const FileUploadModal = ({
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDragOver?.(e);
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
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(getKnowledgeBaseById(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'>
|
||||
<h5 className='modal-title m-0'>上传文件</h5>
|
||||
<button type='button' className='btn-close' onClick={onClose} aria-label='Close'></button>
|
||||
<h5 className='modal-title m-0'>上传文档</h5>
|
||||
<button
|
||||
type='button'
|
||||
className='btn-close'
|
||||
onClick={handleClose}
|
||||
disabled={isUploading}
|
||||
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'
|
||||
fileError ? 'border-danger' : 'border-dashed'
|
||||
}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={handleUploadAreaClick}
|
||||
onDrop={handleDrop}
|
||||
style={{ cursor: isUploading ? 'not-allowed' : 'pointer' }}
|
||||
onClick={!isUploading ? handleUploadAreaClick : undefined}
|
||||
onDrop={!isUploading ? handleDrop : undefined}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
<input
|
||||
type='file'
|
||||
ref={fileInputRef}
|
||||
className='d-none'
|
||||
onChange={onFileChange}
|
||||
accept='.pdf,.docx,.txt,.csv'
|
||||
onChange={handleFileChange}
|
||||
accept='.pdf,.doc,.docx,.txt,.md,.csv,.xlsx,.xls'
|
||||
disabled={isUploading}
|
||||
/>
|
||||
{newFile.file ? (
|
||||
{selectedFile ? (
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
{fileError && <div className='text-danger mt-2'>{fileError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
type='button'
|
||||
className='btn btn-dark'
|
||||
onClick={onUpload}
|
||||
disabled={!newFile.file || isSubmitting}
|
||||
className='btn btn-primary'
|
||||
onClick={handleUpload}
|
||||
disabled={!selectedFile || isUploading}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
{isUploading ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
@ -145,7 +198,7 @@ const FileUploadModal = ({
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
'上传'
|
||||
'上传文档'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -674,7 +674,7 @@ export const mockGet = async (url, config = {}) => {
|
||||
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);
|
||||
|
||||
// Simulate network delay
|
||||
@ -857,6 +857,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' } } };
|
||||
};
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
searchKnowledgeBases,
|
||||
requestKnowledgeBaseAccess,
|
||||
getKnowledgeBaseById,
|
||||
uploadDocument,
|
||||
} from './knowledgeBase.thunks';
|
||||
|
||||
const initialState = {
|
||||
@ -27,6 +28,7 @@ const initialState = {
|
||||
batchLoading: false,
|
||||
editStatus: 'idle',
|
||||
requestAccessStatus: 'idle',
|
||||
uploadStatus: 'idle',
|
||||
};
|
||||
|
||||
const knowledgeBaseSlice = createSlice({
|
||||
@ -180,6 +182,18 @@ const knowledgeBaseSlice = createSlice({
|
||||
.addCase(getKnowledgeBaseById.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
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';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -196,3 +196,44 @@ 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user