[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 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'>
上传文档到知识库支持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 */}
{/* <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 = ({
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>

View File

@ -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' } } };
};

View File

@ -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';
});
},
});

View File

@ -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);
}
}
);