diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java index 3fc5458..2366da9 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java @@ -51,10 +51,14 @@ public class DatasetRepositoryImpl extends CrudRepository findByCriteria(IPage page, DatasetPagingQuery query) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .eq(query.getType() != null, Dataset::getDatasetType, query.getType()) - .eq(query.getStatus() != null, Dataset::getStatus, query.getStatus()) - .like(StringUtils.isNotBlank(query.getKeyword()), Dataset::getName, query.getKeyword()) - .like(StringUtils.isNotBlank(query.getKeyword()), Dataset::getDescription, query.getKeyword()); + .eq(query.getType() != null, Dataset::getDatasetType, query.getType()) + .eq(query.getStatus() != null, Dataset::getStatus, query.getStatus()); + + if (StringUtils.isNotBlank(query.getKeyword())) { + wrapper.and(w -> + w.like(Dataset::getName, query.getKeyword()).or() + .like(Dataset::getDescription, query.getKeyword())); + } /* 标签过滤 {@link Tag} diff --git a/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/application/KnowledgeBaseService.java b/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/application/KnowledgeBaseService.java index 84a5636..85f1e76 100644 --- a/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/application/KnowledgeBaseService.java +++ b/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/application/KnowledgeBaseService.java @@ -14,8 +14,8 @@ import com.datamate.rag.indexer.domain.model.RagFile; import com.datamate.rag.indexer.domain.repository.KnowledgeBaseRepository; import com.datamate.rag.indexer.domain.repository.RagFileRepository; import com.datamate.rag.indexer.infrastructure.event.DataInsertedEvent; +import com.datamate.rag.indexer.infrastructure.milvus.MilvusService; import com.datamate.rag.indexer.interfaces.dto.*; -import io.milvus.client.MilvusClient; import io.milvus.param.collection.DropCollectionParam; import io.milvus.param.dml.DeleteParam; import lombok.RequiredArgsConstructor; @@ -42,7 +42,7 @@ public class KnowledgeBaseService { private final RagFileRepository ragFileRepository; private final ApplicationEventPublisher eventPublisher; private final ModelConfigRepository modelConfigRepository; - private final MilvusClient milvusClient; + private final MilvusService milvusService; /** * 创建知识库 @@ -81,7 +81,7 @@ public class KnowledgeBaseService { .orElseThrow(() -> BusinessException.of(KnowledgeBaseErrorCode.KNOWLEDGE_BASE_NOT_FOUND)); knowledgeBaseRepository.removeById(knowledgeBaseId); ragFileRepository.removeByKnowledgeBaseId(knowledgeBaseId); - milvusClient.dropCollection(DropCollectionParam.newBuilder().withCollectionName(knowledgeBase.getName()).build()); + milvusService.getMilvusClient().dropCollection(DropCollectionParam.newBuilder().withCollectionName(knowledgeBase.getName()).build()); } public KnowledgeBaseResp getById(String knowledgeBaseId) { @@ -147,7 +147,7 @@ public class KnowledgeBaseService { KnowledgeBase knowledgeBase = Optional.ofNullable(knowledgeBaseRepository.getById(knowledgeBaseId)) .orElseThrow(() -> BusinessException.of(KnowledgeBaseErrorCode.KNOWLEDGE_BASE_NOT_FOUND)); ragFileRepository.removeByIds(request.getIds()); - milvusClient.delete(DeleteParam.newBuilder() + milvusService.getMilvusClient().delete(DeleteParam.newBuilder() .withCollectionName(knowledgeBase.getName()) .withExpr("metadata[\"rag_file_id\"] in [" + org.apache.commons.lang3.StringUtils.join(request.getIds().stream().map(id -> "\"" + id + "\"").toArray(), ",") + "]") .build()); diff --git a/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/infrastructure/milvus/MilvusService.java b/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/infrastructure/milvus/MilvusService.java index 6f517f5..4001d83 100644 --- a/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/infrastructure/milvus/MilvusService.java +++ b/backend/services/rag-indexer-service/src/main/java/com/datamate/rag/indexer/infrastructure/milvus/MilvusService.java @@ -7,8 +7,8 @@ import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; import io.milvus.client.MilvusClient; import io.milvus.client.MilvusServiceClient; import io.milvus.param.ConnectParam; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; /** @@ -17,6 +17,7 @@ import org.springframework.stereotype.Component; * @author dallas * @since 2025-11-17 */ +@Slf4j @Component public class MilvusService { @Value("${datamate.rag.milvus-host:milvus-standalone}") @@ -24,6 +25,8 @@ public class MilvusService { @Value("${datamate.rag.milvus-port:19530}") private int milvusPort; + private volatile MilvusClient milvusClient; + public EmbeddingStore embeddingStore(EmbeddingModel embeddingModel, String knowledgeBaseName) { return MilvusEmbeddingStore.builder() .host(milvusHost) @@ -33,12 +36,24 @@ public class MilvusService { .build(); } - @Bean - public MilvusClient milvusClient() { - ConnectParam connectParam = ConnectParam.newBuilder() - .withHost(milvusHost) - .withPort(milvusPort) - .build(); - return new MilvusServiceClient(connectParam); + public MilvusClient getMilvusClient() { + if (milvusClient == null) { + synchronized (this) { + if (milvusClient == null) { + try { + ConnectParam connectParam = ConnectParam.newBuilder() + .withHost(milvusHost) + .withPort(milvusPort) + .build(); + milvusClient = new MilvusServiceClient(connectParam); + log.info("Milvus client connected successfully"); + } catch (Exception e) { + log.error("Milvus client connection failed: {}", e.getMessage()); + throw new RuntimeException("Milvus client connection failed", e); + } + } + } + } + return milvusClient; } } diff --git a/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx b/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx index 7a5c00b..acf5a9e 100644 --- a/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx +++ b/frontend/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.tsx @@ -221,7 +221,7 @@ const KnowledgeBaseDetailPage: React.FC = () => { showReload={false} /> - + datasetTypeMap[type].label, - }, - { - dataIndex: "size", - title: "大小", - ellipsis: true, - }, - { - dataIndex: "fileCount", - title: "文件数", - ellipsis: true, - }, -]; - -export default function AddDataDialog({ knowledgeBase }) { +export default function AddDataDialog({ knowledgeBase, onDataAdded }) { const [isOpen, setIsOpen] = useState(false); const { message } = App.useApp(); const [form] = Form.useForm(); - const [fileList, setFileList] = useState([]); + const [currentStep, setCurrentStep] = useState(0); + // 数据集相关状态 + const [datasets, setDatasets] = useState([]); + const [datasetsTotal, setDatasetsTotal] = useState(0); + const [datasetPage, setDatasetPage] = useState(1); + const [datasetSearch, setDatasetSearch] = useState(''); + const [datasetsLoading, setDatasetsLoading] = useState(false); + // 文件相关状态 + const [datasetFiles, setDatasetFiles] = useState([]); + const [filesTotal, setFilesTotal] = useState(0); + const [filesPage, setFilesPage] = useState(0); + const [fileSearch, setFileSearch] = useState('knowledge-base/detail/'); + const [filesLoading, setFilesLoading] = useState(false); + // 已选择的文件,格式:{datasetId: [fileIds]} + const [selectedFilesMap, setSelectedFilesMap] = useState>({}); + // 当前正在查看的数据集 + const [activeDataset, setActiveDataset] = useState(null); - // Form initial values - const [newKB, setNewKB] = useState>({ - dataSource: "dataset", + // 定义分块选项 + const sliceOptions = [ + { label: '默认分块', value: 'DEFAULT_CHUNK' }, + { label: '按章节分块', value: 'CHAPTER_CHUNK' }, + { label: '按段落分块', value: 'PARAGRAPH_CHUNK' }, + { label: '固定长度分块', value: 'FIXED_LENGTH_CHUNK' }, + { label: '自定义分隔符分块', value: 'CUSTOM_SEPARATOR_CHUNK' }, + ]; + + // 定义初始状态 + const [newKB, setNewKB] = useState({ processType: "DEFAULT_CHUNK", chunkSize: 500, - overlap: 50, - datasetIds: [], + overlapSize: 50, + delimiter: '', }); - const [filesTree, setFilesTree] = useState([]); + const steps = [ + { + title: '选择数据集文件', + description: '从多个数据集中选择文件', + }, + { + title: '配置参数', + description: '设置数据处理参数', + }, + { + title: '确认上传', + description: '确认信息并上传', + }, + ]; - const fetchDatasets = async () => { - const { data } = await queryDatasetsUsingGet({ - page: 0, - size: 1000, - type: DatasetType.TEXT, - }); - const datasets = - data.content.map((item) => ({ - ...item, - key: item.id, - title: item.name, - isLeaf: item.fileCount === 0, - disabled: item.fileCount === 0, - })) || []; - setFilesTree(datasets); + // 数据集列表每页大小 + const DATASET_PAGE_SIZE = 6; + + // 获取数据集列表(支持分页和搜索) + const fetchDatasets = async (page = 1, search = '') => { + setDatasetsLoading(true); + try { + const { data } = await queryDatasetsUsingGet({ + page: page, + size: DATASET_PAGE_SIZE, // 每页大小通过接口传递 + type: DatasetType.TEXT, + keyword: search || undefined, // 搜索参数 + }); + setDatasets(data.content || []); + setDatasetsTotal(data.totalElements || 0); + } catch (error) { + message.error('获取数据集失败'); + console.error('获取数据集列表失败:', error); + } finally { + setDatasetsLoading(false); + } }; + // 文件列表每页大小 + const FILES_PAGE_SIZE = 8; + + // 获取数据集文件列表(支持分页和搜索) + const fetchDatasetFiles = async (datasetId, page = 0, search = '') => { + if (!datasetId) return; + + setFilesLoading(true); + try { + const { data } = await queryDatasetFilesUsingGet(datasetId, { + page: page, // 后端使用0-based页码 + size: FILES_PAGE_SIZE, // 每页最多8条数据 + keyword: search || undefined, // 搜索参数 + }); + + // 确保数据格式正确 + if (data && Array.isArray(data.content)) { + setDatasetFiles(data.content || []); + setFilesTotal(data.totalElements || 0); + } else { + setDatasetFiles([]); + setFilesTotal(0); + } + } catch (error) { + message.error('获取数据集文件失败'); + console.error('获取文件列表失败:', error); + } finally { + setFilesLoading(false); + } + }; + + // 初始化时加载数据集 useEffect(() => { - if (isOpen) fetchDatasets(); - }, [isOpen]); + if (isOpen && currentStep === 0) { + fetchDatasets(datasetPage, datasetSearch); + } + }, [isOpen, currentStep, datasetPage, datasetSearch]); - const updateTreeData = (list, key: React.Key, children) => - list.map((node) => { - if (node.key === key) { - return { - ...node, - children, - }; - } - if (node.children) { - return { - ...node, - children: updateTreeData(node.children, key, children), - }; - } - return node; - }); + // 切换数据集时加载对应文件(重置页码为0) + useEffect(() => { + if (activeDataset) { + setFilesPage(0); // 重置页码 + fetchDatasetFiles(activeDataset.id, 0, fileSearch); + } + }, [activeDataset, fileSearch]); - const onLoadFiles = async ({ key, children }) => - new Promise((resolve) => { - if (children) { - resolve(); + // 确保在文件搜索文本变化时重新加载文件 + useEffect(() => { + if (activeDataset && fileSearch !== undefined) { + setFilesPage(0); + fetchDatasetFiles(activeDataset.id, 0, fileSearch); + } + }, [fileSearch, activeDataset]); + + // 当文件页码变化时重新加载文件 + useEffect(() => { + if (activeDataset && filesPage >= 1) { + fetchDatasetFiles(activeDataset.id, filesPage, fileSearch); + } + }, [filesPage]); + + // 处理数据集搜索 + const handleDatasetSearch = () => { + setDatasetPage(1); + fetchDatasets(1, datasetSearch); + }; + + // 处理文件搜索 + const handleFileSearch = (value) => { + setFileSearch(value); + setFilesPage(0); + if (activeDataset) { + fetchDatasetFiles(activeDataset.id, 0, value); + } + }; + + // 处理数据集分页变化 + const handleDatasetPageChange = (page) => { + setDatasetPage(page); + fetchDatasets(page, datasetSearch); + }; + + // 处理文件分页变化 + const handleFilesPageChange = (page) => { + setFilesPage(page); + if (activeDataset) { + fetchDatasetFiles(activeDataset.id, page, fileSearch); + } + }; + + // 切换活动数据集 + const handleDatasetClick = (dataset) => { + setActiveDataset(dataset); + }; + + // 已经在后面定义了handleFileSelect函数,删除重复定义 + + // 处理全选/取消全选 + const handleSelectAll = (e) => { + if (!activeDataset) return; + + const newSelectedFiles = e.target.checked + ? datasetFiles.map(file => file.id) + : []; + + setSelectedFilesMap(prev => ({ + ...prev, + [activeDataset.id]: newSelectedFiles + })); + }; + + // 检查文件是否已选择 + const isFileSelected = (fileId) => { + if (!activeDataset) return false; + return selectedFilesMap[activeDataset.id]?.includes(fileId) || false; + }; + + // 检查当前数据集是否全选 + const isAllSelected = () => { + if (!activeDataset || datasetFiles.length === 0) return false; + const selectedCount = selectedFilesMap[activeDataset.id]?.length || 0; + return selectedCount === datasetFiles.length; + }; + + // 检查是否部分选择 + const isIndeterminate = () => { + if (!activeDataset) return false; + const selectedCount = selectedFilesMap[activeDataset.id]?.length || 0; + return selectedCount > 0 && selectedCount < datasetFiles.length; + }; + + // 获取已选择文件总数 + const getSelectedFilesCount = () => { + return Object.values(selectedFilesMap).reduce((total, ids) => total + ids.length, 0); + }; + + // 获取所有已选择的文件信息 + const getAllSelectedFiles = () => { + const allFiles = []; + for (const [datasetId, fileIds] of Object.entries(selectedFilesMap)) { + // 找到对应的数据集 + const dataset = datasets.find(d => d.id === datasetId); + // 获取文件详情并添加到数组 + if (dataset) { + allFiles.push(...fileIds.map(fileId => ({ + id: fileId, + name: `${dataset.name}/file_${fileId}` // 使用数据集名称作为前缀 + }))); + } + } + return allFiles; + }; + + const handleNext = () => { + // 验证当前步骤 + if (currentStep === 0) { + if (getSelectedFilesCount() === 0) { + message.warning('请至少选择一个文件'); return; } - queryDatasetFilesUsingGet(key, { - page: 0, - size: 1000, - }).then(({ data }) => { - const children = data.content.map((file) => ({ - title: file.fileName, - key: file.id, - isLeaf: true, - })); - setFilesTree((origin) => updateTreeData(origin, key, children)); - resolve(); - }); + } + if (currentStep === 1) { + // 验证切片参数 + if (!newKB.processType) { + message.warning('请选择分块方式'); + return; + } + if (!newKB.chunkSize || Number(newKB.chunkSize) <= 0) { + message.warning('请输入有效的分块大小'); + return; + } + if (!newKB.overlapSize || Number(newKB.overlapSize) < 0) { + message.warning('请输入有效的重叠长度'); + return; + } + if (newKB.processType === 'CUSTOM_SEPARATOR_CHUNK' && !newKB.delimiter) { + message.warning('请输入分隔符'); + return; + } + } + setCurrentStep(currentStep + 1); + }; + + const handlePrev = () => { + setCurrentStep(currentStep - 1); + }; + + // 重置所有状态 + const handleReset = () => { + setCurrentStep(0); + setSelectedFilesMap({}); + setNewKB({ + processType: 'DEFAULT_CHUNK', + chunkSize: 500, + overlapSize: 50, + delimiter: '', }); - - const handleBeforeUpload = (_, files: UploadFile[]) => { - setFileList([...fileList, ...files]); - return false; + setDatasets([]); + setDatasetPage(1); + setDatasetSearch(''); + setDatasetFiles([]); + setFilesPage(1); + setFileSearch(''); + setActiveDataset(null); + setLoadedFilesCache({}); // 清除文件缓存 + form.resetFields(); }; - const handleRemoveFile = (file: UploadFile) => { - setFileList((prev) => prev.filter((f) => f.uid !== file.uid)); + // 用于缓存已加载过的文件信息,避免重复请求 + const [loadedFilesCache, setLoadedFilesCache] = useState>>({}); + + // 优化处理文件选择(确保选择状态在分页切换后保持) + const handleFileSelect = (checkedValues) => { + if (!activeDataset) return; + + // 更新选择的文件 + setSelectedFilesMap(prev => ({ + ...prev, + [activeDataset.id]: checkedValues + })); + + // 缓存当前页面的文件信息 + if (datasetFiles.length > 0) { + setLoadedFilesCache(prev => { + const datasetCache = prev[activeDataset.id] || {}; + // 更新缓存中的文件信息 + datasetFiles.forEach(file => { + datasetCache[file.id] = file; + }); + return { + ...prev, + [activeDataset.id]: datasetCache + }; + }); + } }; + // 当数据集文件加载完成后,缓存文件信息 + useEffect(() => { + if (activeDataset && datasetFiles.length > 0) { + setLoadedFilesCache(prev => { + const datasetCache = prev[activeDataset.id] || {}; + datasetFiles.forEach(file => { + datasetCache[file.id] = file; + }); + return { + ...prev, + [activeDataset.id]: datasetCache + }; + }); + } + }, [activeDataset, datasetFiles]); + const handleAddData = async () => { - await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, { - knowledgeBaseId: knowledgeBase.id, - files: newKB.dataSource === "local" ? fileList : newKB.files, - processType: newKB.processType, - chunkSize: newKB.chunkSize, - overlap: newKB.overlap, - delimiter: newKB.delimiter, - }); - message.success("数据添加成功"); - form.resetFields(); + const selectedFiles = []; + + Object.entries(selectedFilesMap).forEach(([datasetId, fileIds]) => { + fileIds.forEach(fileId => { + // 查找文件信息以获取文件名 + const fileInfo = datasetFiles.find(file => file.id === fileId); + // 根据API定义,需要id和name字段 + selectedFiles.push({ + id: fileId, + name: fileInfo?.name || fileInfo?.fileName || `文件_${fileId}` + }); + }); + }); + + if (selectedFiles.length === 0) { + message.warning('请至少选择一个文件'); + return; + } + + try { + // 构造符合API要求的请求数据 + const requestData = { + files: selectedFiles, + processType: newKB.processType, + chunkSize: Number(newKB.chunkSize), // 确保是数字类型 + overlapSize: Number(newKB.overlapSize), // 确保是数字类型 + delimiter: newKB.delimiter, + }; + + await addKnowledgeBaseFilesUsingPost(knowledgeBase.id, requestData); + + // 先通知父组件刷新数据(确保刷新发生在重置前) + onDataAdded?.(); + + message.success('数据添加成功'); + // 重置状态 + handleReset(); + setIsOpen(false); + } catch (error) { + message.error('数据添加失败,请重试'); + console.error('添加文件失败:', error); + } + }; + + // handleReset函数已在前面定义,删除重复定义 + + const handleModalCancel = () => { + handleReset(); setIsOpen(false); }; + const renderStepContent = () => { + switch (currentStep) { + case 0: + return ( +
+
选择数据集文件
+
+
+ 请从左侧选择数据集,然后在右侧选择需要导入的文件。支持从多个不同数据集中交叉选择文件。 +
+ + {getSelectedFilesCount() > 0 && ( +
+ 已选择 {getSelectedFilesCount()} 个文件(来自 {Object.keys(selectedFilesMap).length} 个数据集) +
+ )} + +
+ {/* 左侧数据集列表(带搜索和分页) */} +
+
+
数据集列表
+
+ setDatasetSearch(e.target.value)} + onPressEnter={handleDatasetSearch} + prefix={} + suffix={ + + } + /> +
+
+ +
+ {datasetsLoading ? ( +
加载中...
+ ) : datasets.length === 0 ? ( + + ) : ( +
+ {datasets.map(dataset => { + const isSelected = selectedFilesMap[dataset.id]?.length > 0; + return ( +
handleDatasetClick(dataset)} + > +
+
{dataset.name}
+
+ 类型: {datasetTypeMap[dataset.datasetType]?.label || '未知'} +
+
+ 文件数: {dataset.fileCount} +
+
+ {isSelected && ( +
+ 已选择 {selectedFilesMap[dataset.id].length} 个文件 +
+ )} +
+ ); + })} +
+ )} +
+ + {/* 数据集分页 */} +
+ `共 ${total} 个数据集`} + size="small" + /> +
+
+ + {/* 右侧文件列表(带搜索和分页) */} +
+
+
+ {activeDataset ? `${activeDataset.name} 的文件` : '文件列表'} +
+ {activeDataset && ( +
+ setFileSearch(e.target.value)} + onPressEnter={handleFileSearch} + prefix={} + suffix={ + + } + /> +
+ )} +
+ +
+ {!activeDataset ? ( + + ) : filesLoading ? ( +
加载中...
+ ) : datasetFiles.length === 0 ? ( + + ) : ( +
+
+ + 每页显示 {Math.min(datasetFiles.length, FILES_PAGE_SIZE)} 条,共 {filesTotal} 条 + + + 全选 + +
+ + +
+ {datasetFiles.map(file => ( +
+ +
{file.name || file.fileName}
+
+ 大小: {file.size || 'N/A'} | 创建时间: {file.createdAt ? new Date(file.createdAt).toLocaleString() : 'N/A'} +
+
+
+ ))} +
+
+
+ )} +
+ + {/* 文件分页 */} + {activeDataset && ( +
+ handleFilesPageChange(page - 1)} // 转换为0-based页码 + showSizeChanger={false} + showQuickJumper + showTotal={(total) => `共 ${total} 个文件`} + size="small" + /> +
+ )} +
+
+
+
+ ); + + case 1: + return ( +
+ setNewKB({ ...newKB, processType: value })} + > + + + setNewKB({ ...newKB, overlapSize: value })} + > + + +
+ + {newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && ( + setNewKB({ ...newKB, delimiter: value })} + > + + + )} + + ); + + case 2: + return ( +
+
+
上传信息确认
+ +
+
+
数据来源:
+
数据集
+
+ +
+
选择的数据集数:
+
{Object.keys(selectedFilesMap).length}
+
+ +
+
文件总数:
+
{getSelectedFilesCount()}
+
+ +
+
分块方式:
+
+ {sliceOptions.find(opt => opt.value === newKB.processType)?.label} +
+
+ +
+
分块大小:
+
{newKB.chunkSize}
+
+ +
+
重叠长度:
+
{newKB.overlapSize}
+
+ + {newKB.processType === "CUSTOM_SEPARATOR_CHUNK" && newKB.delimiter && ( +
+
分隔符:
+
{newKB.delimiter}
+
+ )} + + {/* 显示每个数据集选择的文件数 */} + {Object.keys(selectedFilesMap).length > 0 && ( +
+
数据集文件明细:
+
+ {datasets.map(dataset => { + const selectedCount = selectedFilesMap[dataset.id]?.length || 0; + if (selectedCount === 0) return null; + return ( +
+ {dataset.name}: + {selectedCount} 个文件 +
+ ); + })} +
+
+ )} +
+
+ +
+ 提示:上传后系统将自动处理文件,请耐心等待 +
+
+ ); + + default: + return null; + } + }; + return ( <> + )} + {currentStep < steps.length - 1 ? ( + + ) : ( + + )} + ); -} +} \ No newline at end of file