[dev]add upload file api

This commit is contained in:
susie-laptop 2025-04-01 22:05:43 -04:00
parent d105e2a358
commit ba53185050
5 changed files with 202 additions and 52 deletions

View File

@ -13,6 +13,7 @@ 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';
// //
const departmentGroups = { const departmentGroups = {
@ -47,6 +48,7 @@ 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(() => { useEffect(() => {
@ -93,7 +95,7 @@ export default function SettingsTab({ knowledgeBase }) {
allowed = ['admin', 'member', 'private'].includes(value); allowed = ['admin', 'member', 'private'].includes(value);
} else { } else {
// private // private
allowed = ['admin', 'private'].includes(value); allowed = ['admin', 'private'].includes(value);
} }
if (!allowed) { if (!allowed) {
@ -334,6 +336,26 @@ export default function SettingsTab({ knowledgeBase }) {
availableGroups={availableGroups} 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'>
上传文档到知识库支持PDFWordExcelTXTMarkdown和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 */} {/* User Permissions Manager */}
{/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */} {/* <UserPermissionsManager knowledgeBase={knowledgeBase} /> */}

View File

@ -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 = ({ 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(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'> <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>

View File

@ -674,7 +674,7 @@ export const mockGet = async (url, config = {}) => {
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 +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' } } }; throw { response: { status: 404, data: { message: 'Not found' } } };
}; };

View File

@ -8,6 +8,7 @@ import {
searchKnowledgeBases, searchKnowledgeBases,
requestKnowledgeBaseAccess, requestKnowledgeBaseAccess,
getKnowledgeBaseById, getKnowledgeBaseById,
uploadDocument,
} from './knowledgeBase.thunks'; } from './knowledgeBase.thunks';
const initialState = { const initialState = {
@ -27,6 +28,7 @@ const initialState = {
batchLoading: false, batchLoading: false,
editStatus: 'idle', editStatus: 'idle',
requestAccessStatus: 'idle', requestAccessStatus: 'idle',
uploadStatus: 'idle',
}; };
const knowledgeBaseSlice = createSlice({ const knowledgeBaseSlice = createSlice({
@ -180,6 +182,18 @@ 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';
}); });
}, },
}); });

View File

@ -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';
/** /**
@ -20,7 +20,7 @@ export const fetchKnowledgeBases = createAsyncThunk(
return response.data; return response.data;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return rejectWithValue(error.response?.data.error.message || 'Failed to fetch knowledge bases'); return rejectWithValue(error.response?.data.error.message || 'Failed to fetch knowledge bases');
} }
} }
@ -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);
}
}
);