361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import { Paper, Grid } from '@mui/material';
|
|||
|
|
import { useTheme } from '@mui/material/styles';
|
|||
|
|
import { useAtomValue } from 'jotai/index';
|
|||
|
|
import { selectedModelInfoAtom } from '@/lib/store';
|
|||
|
|
import UploadArea from './components/UploadArea';
|
|||
|
|
import FileList from './components/FileList';
|
|||
|
|
import DeleteConfirmDialog from './components/DeleteConfirmDialog';
|
|||
|
|
import PdfProcessingDialog from './components/PdfProcessingDialog';
|
|||
|
|
import DomainTreeActionDialog from './components/DomainTreeActionDialog';
|
|||
|
|
import FileLoadingProgress from './components/FileLoadingProgress';
|
|||
|
|
import { fileApi, taskApi } from '@/lib/api';
|
|||
|
|
import { getContent, checkMaxSize, checkInvalidFiles, getvalidFiles } from '@/lib/file/file-process';
|
|||
|
|
import { toast } from 'sonner';
|
|||
|
|
|
|||
|
|
export default function FileUploader({
|
|||
|
|
projectId,
|
|||
|
|
onUploadSuccess,
|
|||
|
|
onFileDeleted,
|
|||
|
|
sendToPages,
|
|||
|
|
setPdfStrategy,
|
|||
|
|
pdfStrategy,
|
|||
|
|
selectedViosnModel,
|
|||
|
|
setSelectedViosnModel,
|
|||
|
|
setPageLoading,
|
|||
|
|
taskFileProcessing,
|
|||
|
|
fileTask
|
|||
|
|
}) {
|
|||
|
|
const theme = useTheme();
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [files, setFiles] = useState([]);
|
|||
|
|
const [pdfFiles, setPdfFiles] = useState([]);
|
|||
|
|
const [uploadedFiles, setUploadedFiles] = useState({});
|
|||
|
|
const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
|
|||
|
|
const [uploading, setUploading] = useState(false);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|||
|
|
const [pdfProcessConfirmOpen, setpdfProcessConfirmOpen] = useState(false);
|
|||
|
|
const [fileToDelete, setFileToDelete] = useState({});
|
|||
|
|
const [domainTreeActionOpen, setDomainTreeActionOpen] = useState(false);
|
|||
|
|
const [domainTreeAction, setDomainTreeAction] = useState('');
|
|||
|
|
const [isFirstUpload, setIsFirstUpload] = useState(false);
|
|||
|
|
const [pendingAction, setPendingAction] = useState(null);
|
|||
|
|
const [taskSettings, setTaskSettings] = useState(null);
|
|||
|
|
const [visionModels, setVisionModels] = useState([]);
|
|||
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|||
|
|
const [pageSize] = useState(10);
|
|||
|
|
const [searchFileName, setSearchFileName] = useState('');
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchUploadedFiles();
|
|||
|
|
}, [currentPage, searchFileName]);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理 PDF 处理方式选择
|
|||
|
|
*/
|
|||
|
|
const handleRadioChange = event => {
|
|||
|
|
const modelId = event.target.selectedVision;
|
|||
|
|
setPdfStrategy(event.target.value);
|
|||
|
|
|
|||
|
|
if (event.target.value === 'mineru') {
|
|||
|
|
toast.success(t('textSplit.mineruSelected'));
|
|||
|
|
} else if (event.target.value === 'mineru-local') {
|
|||
|
|
toast.success(t('textSplit.mineruLocalSelected'));
|
|||
|
|
} else if (event.target.value === 'vision') {
|
|||
|
|
const model = visionModels.find(item => item.id === modelId);
|
|||
|
|
toast.success(
|
|||
|
|
t('textSplit.customVisionModelSelected', {
|
|||
|
|
name: model.modelName,
|
|||
|
|
provider: model.projectName
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
toast.success(t('textSplit.defaultSelected'));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取上传的文件列表
|
|||
|
|
* @param {*} page
|
|||
|
|
* @param {*} size
|
|||
|
|
* @param {*} fileName
|
|||
|
|
*/
|
|||
|
|
const fetchUploadedFiles = async (page = currentPage, size = pageSize, fileName = searchFileName) => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const data = await fileApi.getFiles({ projectId, page, size, fileName, t });
|
|||
|
|
setUploadedFiles(data);
|
|||
|
|
|
|||
|
|
setIsFirstUpload(data.total === 0);
|
|||
|
|
|
|||
|
|
const taskData = await taskApi.getProjectTasks(projectId);
|
|||
|
|
setTaskSettings(taskData);
|
|||
|
|
|
|||
|
|
//使用Jotai会出现数据获取的延迟,导致这里模型获取不到,改用localStorage获取模型信息
|
|||
|
|
const model = JSON.parse(localStorage.getItem('modelConfigList'));
|
|||
|
|
|
|||
|
|
//过滤出视觉模型
|
|||
|
|
const visionItems = model.filter(item => item.type === 'vision');
|
|||
|
|
|
|||
|
|
//先默认选择第一个配置的视觉模型
|
|||
|
|
if (visionItems.length > 0) {
|
|||
|
|
setSelectedViosnModel(visionItems[0].id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setVisionModels(visionItems);
|
|||
|
|
} catch (error) {
|
|||
|
|
toast.error(error.message);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理文件选择
|
|||
|
|
*/
|
|||
|
|
const handleFileSelect = event => {
|
|||
|
|
const selectedFiles = Array.from(event.target.files);
|
|||
|
|
|
|||
|
|
checkMaxSize(selectedFiles);
|
|||
|
|
checkInvalidFiles(selectedFiles);
|
|||
|
|
|
|||
|
|
const validFiles = getvalidFiles(selectedFiles);
|
|||
|
|
|
|||
|
|
if (validFiles.length > 0) {
|
|||
|
|
setFiles(prev => [...prev, ...validFiles]);
|
|||
|
|
}
|
|||
|
|
const hasPdfFiles = selectedFiles.filter(file => file.name.endsWith('.pdf'));
|
|||
|
|
if (hasPdfFiles.length > 0) {
|
|||
|
|
setpdfProcessConfirmOpen(true);
|
|||
|
|
setPdfFiles(hasPdfFiles);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从待上传文件列表中移除文件
|
|||
|
|
*/
|
|||
|
|
const removeFile = index => {
|
|||
|
|
const fileToRemove = files[index];
|
|||
|
|
setFiles(prev => prev.filter((_, i) => i !== index));
|
|||
|
|
if (fileToRemove && fileToRemove.name.toLowerCase().endsWith('.pdf')) {
|
|||
|
|
setPdfFiles(prevPdfFiles => prevPdfFiles.filter(pdfFile => pdfFile.name !== fileToRemove.name));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 上传文件
|
|||
|
|
*/
|
|||
|
|
const uploadFiles = async () => {
|
|||
|
|
if (files.length === 0) return;
|
|||
|
|
|
|||
|
|
// 如果是第一次上传,直接走默认逻辑
|
|||
|
|
if (isFirstUpload) {
|
|||
|
|
handleStartUpload('rebuild');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 否则打开领域树操作选择对话框
|
|||
|
|
setDomainTreeAction('upload');
|
|||
|
|
setPendingAction({ type: 'upload' });
|
|||
|
|
setDomainTreeActionOpen(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理领域树操作选择
|
|||
|
|
*/
|
|||
|
|
const handleDomainTreeAction = action => {
|
|||
|
|
setDomainTreeActionOpen(false);
|
|||
|
|
|
|||
|
|
// 执行挂起的操作
|
|||
|
|
if (pendingAction && pendingAction.type === 'upload') {
|
|||
|
|
handleStartUpload(action);
|
|||
|
|
} else if (pendingAction && pendingAction.type === 'delete') {
|
|||
|
|
handleDeleteFile(action);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清除挂起的操作
|
|||
|
|
setPendingAction(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 开始上传文件
|
|||
|
|
*/
|
|||
|
|
const handleStartUpload = async domainTreeActionType => {
|
|||
|
|
setUploading(true);
|
|||
|
|
try {
|
|||
|
|
const uploadedFileInfos = [];
|
|||
|
|
for (const file of files) {
|
|||
|
|
const { fileContent, fileName } = await getContent(file);
|
|||
|
|
const data = await fileApi.uploadFile({ file, projectId, fileContent, fileName, t });
|
|||
|
|
uploadedFileInfos.push({ fileName: data.fileName, fileId: data.fileId });
|
|||
|
|
}
|
|||
|
|
toast.success(t('textSplit.uploadSuccess', { count: files.length }));
|
|||
|
|
setFiles([]);
|
|||
|
|
setCurrentPage(1);
|
|||
|
|
await fetchUploadedFiles();
|
|||
|
|
if (onUploadSuccess) {
|
|||
|
|
await onUploadSuccess(uploadedFileInfos, pdfFiles, domainTreeActionType);
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
toast.error(err.message || t('textSplit.uploadFailed'));
|
|||
|
|
} finally {
|
|||
|
|
setUploading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 打开删除确认对话框
|
|||
|
|
const openDeleteConfirm = (fileId, fileName) => {
|
|||
|
|
setFileToDelete({ fileId, fileName });
|
|||
|
|
setDeleteConfirmOpen(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 关闭删除确认对话框
|
|||
|
|
const closeDeleteConfirm = () => {
|
|||
|
|
setDeleteConfirmOpen(false);
|
|||
|
|
setFileToDelete(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 删除文件前确认领域树操作
|
|||
|
|
const confirmDeleteFile = () => {
|
|||
|
|
setDeleteConfirmOpen(false);
|
|||
|
|
|
|||
|
|
// 如果没有其他文件了(删除后会变为空),直接删除
|
|||
|
|
if (uploadedFiles.total <= 1) {
|
|||
|
|
handleDeleteFile('keep');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 否则打开领域树操作选择对话框
|
|||
|
|
setDomainTreeAction('delete');
|
|||
|
|
setPendingAction({ type: 'delete' });
|
|||
|
|
setDomainTreeActionOpen(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理删除文件
|
|||
|
|
const handleDeleteFile = async domainTreeActionType => {
|
|||
|
|
if (!fileToDelete) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
closeDeleteConfirm();
|
|||
|
|
|
|||
|
|
await fileApi.deleteFile({
|
|||
|
|
fileToDelete,
|
|||
|
|
projectId,
|
|||
|
|
domainTreeActionType,
|
|||
|
|
modelInfo: selectedModelInfo || {},
|
|||
|
|
t
|
|||
|
|
});
|
|||
|
|
await fetchUploadedFiles();
|
|||
|
|
|
|||
|
|
if (onFileDeleted) {
|
|||
|
|
const filesLength = uploadedFiles.total;
|
|||
|
|
onFileDeleted(fileToDelete, filesLength);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (uploadedFiles.data && uploadedFiles.data.length <= 1 && currentPage > 1) {
|
|||
|
|
setCurrentPage(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toast.success(t('textSplit.deleteSuccess', { fileName: fileToDelete.fileName }));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error deleting file:', error);
|
|||
|
|
toast.error(error.message);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
setFileToDelete(null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Paper
|
|||
|
|
elevation={0}
|
|||
|
|
sx={{
|
|||
|
|
p: 3,
|
|||
|
|
mb: 3,
|
|||
|
|
border: `1px solid ${theme.palette.divider}`,
|
|||
|
|
borderRadius: 2
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{taskFileProcessing ? (
|
|||
|
|
<FileLoadingProgress fileTask={fileTask} />
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<Grid container spacing={3}>
|
|||
|
|
{/* 左侧:上传文件区域 */}
|
|||
|
|
<Grid item xs={10} md={5} sx={{ maxWidth: '100%', width: '100%' }}>
|
|||
|
|
<UploadArea
|
|||
|
|
theme={theme}
|
|||
|
|
files={files}
|
|||
|
|
uploading={uploading}
|
|||
|
|
uploadedFiles={uploadedFiles}
|
|||
|
|
onFileSelect={handleFileSelect}
|
|||
|
|
onRemoveFile={removeFile}
|
|||
|
|
onUpload={uploadFiles}
|
|||
|
|
selectedModel={selectedModelInfo}
|
|||
|
|
/>
|
|||
|
|
</Grid>
|
|||
|
|
|
|||
|
|
{/* 右侧:已上传文件列表 */}
|
|||
|
|
<Grid item xs={14} md={7} sx={{ maxWidth: '100%', width: '100%' }}>
|
|||
|
|
<FileList
|
|||
|
|
theme={theme}
|
|||
|
|
files={uploadedFiles}
|
|||
|
|
loading={loading}
|
|||
|
|
setPageLoading={setPageLoading}
|
|||
|
|
sendToFileUploader={array => sendToPages(array)}
|
|||
|
|
onDeleteFile={openDeleteConfirm}
|
|||
|
|
projectId={projectId}
|
|||
|
|
currentPage={currentPage}
|
|||
|
|
onPageChange={(page, fileName) => {
|
|||
|
|
if (fileName !== undefined) {
|
|||
|
|
// 搜索时更新搜索关键词和页码
|
|||
|
|
setSearchFileName(fileName);
|
|||
|
|
setCurrentPage(page);
|
|||
|
|
} else {
|
|||
|
|
// 翻页时只更新页码
|
|||
|
|
setCurrentPage(page);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
onRefresh={fetchUploadedFiles} // 传递刷新函数
|
|||
|
|
/>
|
|||
|
|
</Grid>
|
|||
|
|
</Grid>
|
|||
|
|
|
|||
|
|
<DeleteConfirmDialog
|
|||
|
|
open={deleteConfirmOpen}
|
|||
|
|
fileName={fileToDelete?.fileName}
|
|||
|
|
onClose={closeDeleteConfirm}
|
|||
|
|
onConfirm={confirmDeleteFile}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* 领域树操作选择对话框 */}
|
|||
|
|
<DomainTreeActionDialog
|
|||
|
|
open={domainTreeActionOpen}
|
|||
|
|
onClose={() => setDomainTreeActionOpen(false)}
|
|||
|
|
onConfirm={handleDomainTreeAction}
|
|||
|
|
isFirstUpload={isFirstUpload}
|
|||
|
|
action={domainTreeAction}
|
|||
|
|
/>
|
|||
|
|
{/* 检测到pdf的处理框 */}
|
|||
|
|
<PdfProcessingDialog
|
|||
|
|
open={pdfProcessConfirmOpen}
|
|||
|
|
onClose={() => setpdfProcessConfirmOpen(false)}
|
|||
|
|
onRadioChange={handleRadioChange}
|
|||
|
|
value={pdfStrategy}
|
|||
|
|
projectId={projectId}
|
|||
|
|
taskSettings={taskSettings}
|
|||
|
|
visionModels={visionModels}
|
|||
|
|
selectedViosnModel={selectedViosnModel}
|
|||
|
|
setSelectedViosnModel={setSelectedViosnModel}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</Paper>
|
|||
|
|
);
|
|||
|
|
}
|