You've already forked DataMate
- 集成LibreOffice转换器实现DOC/DOCX转PDF功能 - 新增DatasetFilePreviewService处理预览文件管理 - 新增DatasetFilePreviewAsyncService异步转换任务 - 在文件删除时同步清理预览文件 - 前端实现Office文档预览状态轮询机制 - 添加预览API接口支持状态查询和转换触发 - 优化文件预览界面显示转换进度和错误信息
509 lines
15 KiB
TypeScript
509 lines
15 KiB
TypeScript
import {
|
|
App,
|
|
Button,
|
|
Descriptions,
|
|
DescriptionsProps,
|
|
Modal,
|
|
Spin,
|
|
Table,
|
|
Input,
|
|
} from "antd";
|
|
import { formatBytes, formatDateTime } from "@/utils/unit";
|
|
import { Download, Trash2, Folder, File } from "lucide-react";
|
|
import { datasetTypeMap } from "../../dataset.const";
|
|
import type { Dataset, DatasetFile } from "@/pages/DataManagement/dataset.model";
|
|
import type { useFilesOperation } from "../useFilesOperation";
|
|
|
|
type DatasetFileRow = DatasetFile & {
|
|
fileSize?: number;
|
|
fileCount?: number;
|
|
uploadTime?: string;
|
|
};
|
|
|
|
const PREVIEW_MAX_HEIGHT = 500;
|
|
const PREVIEW_MODAL_WIDTH = {
|
|
text: "80vw",
|
|
media: "80vw",
|
|
};
|
|
const PREVIEW_TEXT_FONT_SIZE = 12;
|
|
const PREVIEW_TEXT_PADDING = 12;
|
|
const PREVIEW_AUDIO_PADDING = 40;
|
|
|
|
type OverviewProps = {
|
|
dataset: Dataset;
|
|
filesOperation: ReturnType<typeof useFilesOperation>;
|
|
fetchDataset: () => void;
|
|
onUpload?: () => void;
|
|
};
|
|
|
|
export default function Overview({
|
|
dataset,
|
|
filesOperation,
|
|
fetchDataset,
|
|
onUpload,
|
|
}: OverviewProps) {
|
|
const { modal, message } = App.useApp();
|
|
const {
|
|
fileList,
|
|
pagination,
|
|
selectedFiles,
|
|
previewVisible,
|
|
previewFileName,
|
|
previewContent,
|
|
previewFileType,
|
|
previewMediaUrl,
|
|
previewLoading,
|
|
officePreviewStatus,
|
|
officePreviewError,
|
|
closePreview,
|
|
handleDeleteFile,
|
|
handleDownloadFile,
|
|
handleBatchDeleteFiles,
|
|
handleBatchExport,
|
|
handleCreateDirectory,
|
|
handleDownloadDirectory,
|
|
handleDeleteDirectory,
|
|
handlePreviewFile,
|
|
} = filesOperation;
|
|
|
|
// 基本信息
|
|
const items: DescriptionsProps["items"] = [
|
|
{
|
|
key: "id",
|
|
label: "ID",
|
|
children: dataset.id,
|
|
},
|
|
{
|
|
key: "name",
|
|
label: "名称",
|
|
children: dataset.name,
|
|
},
|
|
{
|
|
key: "fileCount",
|
|
label: "文件数",
|
|
children: dataset.fileCount || 0,
|
|
},
|
|
{
|
|
key: "size",
|
|
label: "数据大小",
|
|
children: dataset.size || "0 B",
|
|
},
|
|
|
|
{
|
|
key: "datasetType",
|
|
label: "类型",
|
|
children: datasetTypeMap[dataset?.datasetType]?.label || "未知",
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "状态",
|
|
children: dataset?.status?.label || "未知",
|
|
},
|
|
{
|
|
key: "createdBy",
|
|
label: "创建者",
|
|
children: dataset.createdBy || "未知",
|
|
},
|
|
{
|
|
key: "createdAt",
|
|
label: "创建时间",
|
|
children: dataset.createdAt,
|
|
},
|
|
{
|
|
key: "updatedAt",
|
|
label: "更新时间",
|
|
children: dataset.updatedAt,
|
|
},
|
|
{
|
|
key: "description",
|
|
label: "描述",
|
|
children: dataset.description || "无",
|
|
},
|
|
];
|
|
|
|
// 文件列表列定义
|
|
const columns = [
|
|
{
|
|
title: "文件名",
|
|
dataIndex: "fileName",
|
|
key: "fileName",
|
|
fixed: "left",
|
|
render: (text: string, record: DatasetFileRow) => {
|
|
const isDirectory = record.id.startsWith('directory-');
|
|
const iconSize = 16;
|
|
|
|
const content = (
|
|
<div className="flex items-center">
|
|
{isDirectory ? (
|
|
<Folder className="mr-2 text-blue-500" size={iconSize} />
|
|
) : (
|
|
<File className="mr-2 text-black" size={iconSize} />
|
|
)}
|
|
<span className="truncate text-black">{text}</span>
|
|
</div>
|
|
);
|
|
|
|
if (isDirectory) {
|
|
return (
|
|
<Button
|
|
type="link"
|
|
onClick={() => {
|
|
const currentPath = filesOperation.pagination.prefix || '';
|
|
// 文件夹路径必须以斜杠结尾
|
|
const newPath = `${currentPath}${record.fileName}/`;
|
|
filesOperation.fetchFiles(newPath, 1, filesOperation.pagination.pageSize);
|
|
}}
|
|
>
|
|
{content}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
type="link"
|
|
loading={previewLoading && previewFileName === record.fileName}
|
|
onClick={() => handlePreviewFile(record)}
|
|
>
|
|
{content}
|
|
</Button>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: "大小",
|
|
dataIndex: "fileSize",
|
|
key: "fileSize",
|
|
width: 150,
|
|
render: (text: number, record: DatasetFileRow) => {
|
|
const isDirectory = record.id.startsWith('directory-');
|
|
if (isDirectory) {
|
|
return formatBytes(record.fileSize || 0);
|
|
}
|
|
return formatBytes(text)
|
|
},
|
|
},
|
|
{
|
|
title: "包含文件数",
|
|
dataIndex: "fileCount",
|
|
key: "fileCount",
|
|
width: 120,
|
|
render: (text: number, record: DatasetFileRow) => {
|
|
const isDirectory = record.id.startsWith('directory-');
|
|
if (!isDirectory) {
|
|
return "-";
|
|
}
|
|
return record.fileCount ?? 0;
|
|
},
|
|
},
|
|
{
|
|
title: "上传时间",
|
|
dataIndex: "uploadTime",
|
|
key: "uploadTime",
|
|
width: 200,
|
|
render: (text) => formatDateTime(text),
|
|
},
|
|
{
|
|
title: "操作",
|
|
key: "action",
|
|
width: 180,
|
|
fixed: "right",
|
|
render: (_, record: DatasetFileRow) => {
|
|
const isDirectory = record.id.startsWith('directory-');
|
|
|
|
if (isDirectory) {
|
|
const currentPath = filesOperation.pagination.prefix || '';
|
|
const fullPath = `${currentPath}${record.fileName}/`;
|
|
|
|
return (
|
|
<div className="flex">
|
|
<Button
|
|
size="small"
|
|
type="link"
|
|
onClick={() => handleDownloadDirectory(fullPath, record.fileName)}
|
|
>
|
|
下载
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
type="link"
|
|
onClick={() => {
|
|
modal.confirm({
|
|
title: '确认删除文件夹?',
|
|
content: `删除文件夹 "${record.fileName}" 将同时删除其中的所有文件和子文件夹,此操作不可恢复。`,
|
|
okText: '删除',
|
|
okType: 'danger',
|
|
cancelText: '取消',
|
|
onOk: async () => {
|
|
await handleDeleteDirectory(fullPath, record.fileName);
|
|
fetchDataset();
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
删除
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex">
|
|
<Button
|
|
size="small"
|
|
type="link"
|
|
loading={previewLoading && previewFileName === record.fileName}
|
|
onClick={() => handlePreviewFile(record)}
|
|
>
|
|
预览
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
type="link"
|
|
onClick={() => handleDownloadFile(record)}
|
|
>
|
|
下载
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
type="link"
|
|
onClick={async () => {
|
|
await handleDeleteFile(record);
|
|
fetchDataset()
|
|
}
|
|
}
|
|
>
|
|
删除
|
|
</Button>
|
|
</div>
|
|
)},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div className=" flex flex-col gap-4">
|
|
{/* 基本信息 */}
|
|
<Descriptions
|
|
title="基本信息"
|
|
layout="vertical"
|
|
size="small"
|
|
items={items}
|
|
column={5}
|
|
/>
|
|
|
|
{/* 文件列表 */}
|
|
<div className="flex items-center justify-between mt-8 mb-2">
|
|
<h2 className="text-base font-semibold">文件列表</h2>
|
|
<div className="flex items-center gap-2">
|
|
<Button size="small" onClick={() => onUpload?.()}>
|
|
文件上传
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
onClick={() => {
|
|
let dirName = "";
|
|
modal.confirm({
|
|
title: "新建文件夹",
|
|
content: (
|
|
<Input
|
|
autoFocus
|
|
placeholder="请输入文件夹名称"
|
|
onChange={(e) => {
|
|
dirName = e.target.value?.trim();
|
|
}}
|
|
/>
|
|
),
|
|
okText: "确定",
|
|
cancelText: "取消",
|
|
onOk: async () => {
|
|
if (!dirName) {
|
|
message.warning("请输入文件夹名称");
|
|
return Promise.reject();
|
|
}
|
|
await handleCreateDirectory(dirName);
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
新建文件夹
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{selectedFiles.length > 0 && (
|
|
<div className="flex items-center gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
|
<span className="text-sm text-blue-700 font-medium">
|
|
已选择 {selectedFiles.length} 个文件
|
|
</span>
|
|
<Button
|
|
onClick={handleBatchExport}
|
|
className="ml-auto bg-transparent"
|
|
>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
批量导出
|
|
</Button>
|
|
<Button
|
|
onClick={handleBatchDeleteFiles}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50 bg-transparent"
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
批量删除
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<div className="overflow-x-auto">
|
|
<div className="mb-2">
|
|
{(filesOperation.pagination.prefix || '') !== '' && (
|
|
<Button
|
|
type="link"
|
|
onClick={() => {
|
|
// 获取上一级目录
|
|
const currentPath = filesOperation.pagination.prefix || '';
|
|
// 移除末尾的斜杠,然后按斜杠分割
|
|
const trimmedPath = currentPath.replace(/\/$/, '');
|
|
const pathParts = trimmedPath.split('/');
|
|
// 移除最后一个目录名
|
|
pathParts.pop();
|
|
// 重新组合路径,如果还有内容则加斜杠,否则为空
|
|
const parentPath = pathParts.length > 0 ? `${pathParts.join('/')}/` : '';
|
|
filesOperation.fetchFiles(parentPath, 1, filesOperation.pagination.pageSize);
|
|
}}
|
|
className="p-0"
|
|
>
|
|
<span className="flex items-center text-blue-500">
|
|
<svg
|
|
className="w-4 h-4 mr-1"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
/>
|
|
</svg>
|
|
返回上一级
|
|
</span>
|
|
</Button>
|
|
)}
|
|
{filesOperation.pagination.prefix && (
|
|
<span className="ml-2 text-gray-600">当前路径: {filesOperation.pagination.prefix}</span>
|
|
)}
|
|
</div>
|
|
<Table
|
|
size="middle"
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={fileList}
|
|
// rowSelection={rowSelection}
|
|
scroll={{ x: "max-content", y: 600 }}
|
|
pagination={{
|
|
...pagination,
|
|
showTotal: (total) => `共 ${total} 条`,
|
|
onChange: (page, pageSize) => {
|
|
filesOperation.fetchFiles(filesOperation.pagination.prefix, page, pageSize);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* 文件预览弹窗 */}
|
|
<Modal
|
|
title={`文件预览:${previewFileName}`}
|
|
open={previewVisible}
|
|
onCancel={closePreview}
|
|
footer={[
|
|
<Button key="close" onClick={closePreview}>
|
|
关闭
|
|
</Button>,
|
|
]}
|
|
width={previewFileType === "text" ? PREVIEW_MODAL_WIDTH.text : PREVIEW_MODAL_WIDTH.media}
|
|
>
|
|
{previewFileType === "text" && (
|
|
<pre
|
|
style={{
|
|
maxHeight: `${PREVIEW_MAX_HEIGHT}px`,
|
|
overflow: "auto",
|
|
whiteSpace: "pre-wrap",
|
|
wordBreak: "break-all",
|
|
fontSize: PREVIEW_TEXT_FONT_SIZE,
|
|
color: "#222",
|
|
backgroundColor: "#f5f5f5",
|
|
padding: `${PREVIEW_TEXT_PADDING}px`,
|
|
borderRadius: "4px",
|
|
}}
|
|
>
|
|
{previewContent}
|
|
</pre>
|
|
)}
|
|
{previewFileType === "image" && (
|
|
<div style={{ textAlign: "center" }}>
|
|
<img
|
|
src={previewMediaUrl}
|
|
alt={previewFileName}
|
|
style={{ maxWidth: "100%", maxHeight: `${PREVIEW_MAX_HEIGHT}px`, objectFit: "contain" }}
|
|
/>
|
|
</div>
|
|
)}
|
|
{previewFileType === "pdf" && (
|
|
<>
|
|
{previewMediaUrl ? (
|
|
<iframe
|
|
src={previewMediaUrl}
|
|
title={previewFileName || "PDF 预览"}
|
|
style={{ width: "100%", height: `${PREVIEW_MAX_HEIGHT}px`, border: "none" }}
|
|
/>
|
|
) : (
|
|
<div
|
|
style={{
|
|
height: `${PREVIEW_MAX_HEIGHT}px`,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 12,
|
|
color: "#666",
|
|
}}
|
|
>
|
|
{officePreviewStatus === "FAILED" ? (
|
|
<>
|
|
<div>转换失败</div>
|
|
<div>{officePreviewError || "请稍后重试"}</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Spin />
|
|
<div>正在转换,请稍候...</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
{previewFileType === "video" && (
|
|
<div style={{ textAlign: "center" }}>
|
|
<video
|
|
src={previewMediaUrl}
|
|
controls
|
|
style={{ maxWidth: "100%", maxHeight: `${PREVIEW_MAX_HEIGHT}px` }}
|
|
>
|
|
您的浏览器不支持视频播放
|
|
</video>
|
|
</div>
|
|
)}
|
|
{previewFileType === "audio" && (
|
|
<div style={{ textAlign: "center", padding: `${PREVIEW_AUDIO_PADDING}px 0` }}>
|
|
<audio src={previewMediaUrl} controls style={{ width: "100%" }}>
|
|
您的浏览器不支持音频播放
|
|
</audio>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|