import type { Dataset, DatasetFile, } from "@/pages/DataManagement/dataset.model"; import { App } from "antd"; import { useCallback, useEffect, useRef, useState } from "react"; import { PREVIEW_TEXT_MAX_LENGTH, resolvePreviewFileType, truncatePreviewText, type PreviewFileType, } from "@/utils/filePreview"; import { deleteDatasetFileUsingDelete, downloadFileByIdUsingGet, exportDatasetUsingPost, queryDatasetFilesUsingGet, createDatasetDirectoryUsingPost, downloadDirectoryUsingGet, deleteDirectoryUsingDelete, queryDatasetFilePreviewStatusUsingGet, convertDatasetFilePreviewUsingPost, } from "../dataset.api"; import { useParams } from "react-router"; const OFFICE_FILE_EXTENSIONS = [".doc", ".docx"]; const OFFICE_PREVIEW_POLL_INTERVAL = 2000; const OFFICE_PREVIEW_POLL_MAX_TIMES = 60; type OfficePreviewStatus = "UNSET" | "PENDING" | "PROCESSING" | "READY" | "FAILED"; const isOfficeFileName = (fileName?: string) => { const lowerName = (fileName || "").toLowerCase(); return OFFICE_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext)); }; const normalizeOfficePreviewStatus = (status?: string): OfficePreviewStatus => { if (!status) { return "UNSET"; } const upper = status.toUpperCase(); if (upper === "PENDING" || upper === "PROCESSING" || upper === "READY" || upper === "FAILED") { return upper as OfficePreviewStatus; } return "UNSET"; }; export function useFilesOperation(dataset: Dataset) { const { message } = App.useApp(); const { id } = useParams(); // 获取动态路由参数 // 文件相关状态 const [fileList, setFileList] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [pagination, setPagination] = useState<{ current: number; pageSize: number; total: number; prefix?: string; }>({ current: 1, pageSize: 10, total: 0, prefix: '' }); // 文件预览相关状态 const [previewVisible, setPreviewVisible] = useState(false); const [previewContent, setPreviewContent] = useState(""); const [previewFileName, setPreviewFileName] = useState(""); const [previewFileType, setPreviewFileType] = useState("text"); const [previewMediaUrl, setPreviewMediaUrl] = useState(""); const [previewLoading, setPreviewLoading] = useState(false); const [officePreviewStatus, setOfficePreviewStatus] = useState(null); const [officePreviewError, setOfficePreviewError] = useState(""); const officePreviewPollingRef = useRef(null); const officePreviewFileRef = useRef(null); const clearOfficePreviewPolling = useCallback(() => { if (officePreviewPollingRef.current) { window.clearTimeout(officePreviewPollingRef.current); officePreviewPollingRef.current = null; } }, []); useEffect(() => { return () => { clearOfficePreviewPolling(); }; }, [clearOfficePreviewPolling]); const fetchFiles = async ( prefix?: string, current?: number, pageSize?: number ) => { // 如果明确传了 prefix(包括空字符串),使用传入的值;否则使用当前 pagination.prefix const targetPrefix = prefix !== undefined ? prefix : (pagination.prefix || ''); const params: DatasetFilesQueryParams = { page: current !== undefined ? current : pagination.current, size: pageSize !== undefined ? pageSize : pagination.pageSize, isWithDirectory: true, prefix: targetPrefix, excludeDerivedFiles: true, }; const { data } = await queryDatasetFilesUsingGet(id!, params); setFileList(data.content || []); // Update pagination with current prefix setPagination(prev => ({ ...prev, current: params.page, pageSize: params.size, prefix: targetPrefix, total: data.totalElements || 0, })); }; const handleBatchDeleteFiles = () => { if (selectedFiles.length === 0) { message.warning({ content: "请先选择要删除的文件" }); return; } // 执行批量删除逻辑 selectedFiles.forEach(async (fileId) => { await fetch(`/api/datasets/${dataset.id}/files/${fileId}`, { method: "DELETE", }); }); fetchFiles(); // 刷新文件列表 setSelectedFiles([]); // 清空选中状态 message.success({ content: `已删除 ${selectedFiles.length} 个文件`, }); }; const handleDownloadFile = async (file: DatasetFile) => { // 实际导出逻辑 await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName); // 假设导出成功 message.success({ content: `已导出 1 个文件`, }); setSelectedFiles([]); // 清空选中状态 }; const handlePreviewFile = async (file: DatasetFile) => { const datasetId = dataset?.id || id; if (!datasetId) { message.warning({ content: "数据集未就绪" }); return; } if (file?.id?.startsWith("directory-")) { return; } const previewUrl = `/api/data-management/datasets/${datasetId}/files/${file.id}/preview`; setPreviewFileName(file.fileName); setPreviewContent(""); setPreviewMediaUrl(""); if (isOfficeFileName(file?.fileName)) { setPreviewFileType("pdf"); setPreviewVisible(true); setPreviewLoading(true); setOfficePreviewStatus("PROCESSING"); setOfficePreviewError(""); officePreviewFileRef.current = file.id; try { const { data: statusData } = await queryDatasetFilePreviewStatusUsingGet(datasetId, file.id); const currentStatus = normalizeOfficePreviewStatus(statusData?.status); if (currentStatus === "READY") { setPreviewMediaUrl(previewUrl); setOfficePreviewStatus("READY"); setPreviewLoading(false); return; } if (currentStatus === "PROCESSING") { pollOfficePreviewStatus(datasetId, file.id, 0); return; } const { data } = await convertDatasetFilePreviewUsingPost(datasetId, file.id); const status = normalizeOfficePreviewStatus(data?.status); if (status === "READY") { setPreviewMediaUrl(previewUrl); setOfficePreviewStatus("READY"); } else if (status === "FAILED") { setOfficePreviewStatus("FAILED"); setOfficePreviewError(data?.previewError || "转换失败,请稍后重试"); } else { setOfficePreviewStatus("PROCESSING"); pollOfficePreviewStatus(datasetId, file.id, 0); return; } } catch (error) { console.error("触发预览转换失败", error); message.error({ content: "触发预览转换失败" }); setOfficePreviewStatus("FAILED"); setOfficePreviewError("触发预览转换失败"); } finally { setPreviewLoading(false); } return; } const fileType = resolvePreviewFileType(file?.fileName); if (!fileType) { message.warning({ content: "不支持预览该文件类型" }); return; } setPreviewFileType(fileType); if (fileType === "text") { setPreviewLoading(true); try { const response = await fetch(previewUrl); if (!response.ok) { throw new Error("下载失败"); } const text = await response.text(); setPreviewContent(truncatePreviewText(text, PREVIEW_TEXT_MAX_LENGTH)); setPreviewVisible(true); } catch (error) { console.error("Preview file content error:", error); message.error({ content: "获取文件内容失败" }); } finally { setPreviewLoading(false); } return; } setPreviewMediaUrl(previewUrl); setPreviewVisible(true); }; const closePreview = () => { clearOfficePreviewPolling(); officePreviewFileRef.current = null; setPreviewVisible(false); setPreviewContent(""); setPreviewMediaUrl(""); setPreviewFileName(""); setPreviewFileType("text"); setOfficePreviewStatus(null); setOfficePreviewError(""); }; const pollOfficePreviewStatus = useCallback( async (datasetId: string, fileId: string, attempt: number) => { clearOfficePreviewPolling(); officePreviewPollingRef.current = window.setTimeout(async () => { if (officePreviewFileRef.current !== fileId) { return; } try { const { data } = await queryDatasetFilePreviewStatusUsingGet(datasetId, fileId); const status = normalizeOfficePreviewStatus(data?.status); if (status === "READY") { setPreviewMediaUrl(`/api/data-management/datasets/${datasetId}/files/${fileId}/preview`); setOfficePreviewStatus("READY"); setOfficePreviewError(""); setPreviewLoading(false); return; } if (status === "FAILED") { setOfficePreviewStatus("FAILED"); setOfficePreviewError(data?.previewError || "转换失败,请稍后重试"); setPreviewLoading(false); return; } if (attempt >= OFFICE_PREVIEW_POLL_MAX_TIMES - 1) { setOfficePreviewStatus("FAILED"); setOfficePreviewError("转换超时,请稍后重试"); setPreviewLoading(false); return; } pollOfficePreviewStatus(datasetId, fileId, attempt + 1); } catch (error) { console.error("轮询预览状态失败", error); if (attempt >= OFFICE_PREVIEW_POLL_MAX_TIMES - 1) { setOfficePreviewStatus("FAILED"); setOfficePreviewError("转换超时,请稍后重试"); setPreviewLoading(false); return; } pollOfficePreviewStatus(datasetId, fileId, attempt + 1); } }, OFFICE_PREVIEW_POLL_INTERVAL); }, [clearOfficePreviewPolling] ); const handleDeleteFile = async (file: DatasetFile) => { try { await deleteDatasetFileUsingDelete(dataset.id, file.id); fetchFiles(); // 刷新文件列表 message.success({ content: `文件 ${file.fileName} 已删除` }); } catch { message.error({ content: `文件 ${file.fileName} 删除失败` }); } }; const handleBatchExport = () => { if (selectedFiles.length === 0) { message.warning({ content: "请先选择要导出的文件" }); return; } // 执行批量导出逻辑 exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles }) .then(() => { message.success({ content: `已导出 ${selectedFiles.length} 个文件`, }); setSelectedFiles([]); // 清空选中状态 }) .catch(() => { message.error({ content: "导出失败,请稍后再试", }); }); }; return { fileList, selectedFiles, setSelectedFiles, pagination, setPagination, previewVisible, previewContent, previewFileName, previewFileType, previewMediaUrl, previewLoading, officePreviewStatus, officePreviewError, closePreview, fetchFiles, setFileList, handleBatchDeleteFiles, handleDownloadFile, handlePreviewFile, handleDeleteFile, handleBatchExport, handleCreateDirectory: async (directoryName: string) => { const currentPrefix = pagination.prefix || ""; try { await createDatasetDirectoryUsingPost(dataset.id, { parentPrefix: currentPrefix, directoryName, }); // 创建成功后刷新当前目录,重置到第一页 await fetchFiles(currentPrefix, 1, pagination.pageSize); message.success({ content: `文件夹 ${directoryName} 创建成功` }); } catch (caught) { message.error({ content: `文件夹 ${directoryName} 创建失败` }); throw caught; } }, handleDownloadDirectory: async (directoryPath: string, directoryName: string) => { try { await downloadDirectoryUsingGet(dataset.id, directoryPath); message.success({ content: `文件夹 ${directoryName} 下载成功` }); } catch { message.error({ content: `文件夹 ${directoryName} 下载失败` }); } }, handleDeleteDirectory: async (directoryPath: string, directoryName: string) => { try { await deleteDirectoryUsingDelete(dataset.id, directoryPath); // 删除成功后刷新当前目录 const currentPrefix = pagination.prefix || ""; await fetchFiles(currentPrefix, 1, pagination.pageSize); message.success({ content: `文件夹 ${directoryName} 已删除` }); } catch { message.error({ content: `文件夹 ${directoryName} 删除失败` }); } }, }; } interface DatasetFilesQueryParams { page: number; size: number; isWithDirectory: boolean; prefix: string; excludeDerivedFiles?: boolean; }