You've already forked DataMate
feat: fix the problem in the Operator Market frontend pages
This commit is contained in:
@@ -1,138 +1,138 @@
|
||||
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
|
||||
import { Button, Card, Descriptions, Progress } from "antd";
|
||||
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {formatExecutionDuration} from "@/utils/unit.ts";
|
||||
|
||||
export default function BasicInfo({ task }: { task: CleansingTask }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const descriptionItems = [
|
||||
{
|
||||
key: "id",
|
||||
label: "任务ID",
|
||||
children: <span className="font-mono">{task?.id}</span>,
|
||||
},
|
||||
{ key: "name", label: "任务名称", children: task?.name },
|
||||
{
|
||||
key: "dataset",
|
||||
label: "源数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.srcDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.srcDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "targetDataset",
|
||||
label: "目标数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.destDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.destDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ key: "startTime", label: "开始时间", children: task?.startedAt },
|
||||
{
|
||||
key: "description",
|
||||
label: "任务描述",
|
||||
children: (
|
||||
<span className="text-gray-600">{task?.description || "--"}</span>
|
||||
),
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 执行摘要 */}
|
||||
<Card className="mb-6">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
|
||||
<Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-blue-500">
|
||||
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总耗时</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
|
||||
<CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-green-500">
|
||||
{task?.progress?.succeedFileNum || "0"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-red-500">
|
||||
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">失败文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
|
||||
<Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-purple-500">
|
||||
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功率</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 基本信息 */}
|
||||
<Card>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
|
||||
<Descriptions
|
||||
column={2}
|
||||
bordered={false}
|
||||
size="middle"
|
||||
labelStyle={{ fontWeight: 500, color: "#555" }}
|
||||
contentStyle={{ fontSize: 14 }}
|
||||
items={descriptionItems}
|
||||
></Descriptions>
|
||||
</div>
|
||||
{/* 处理进度 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">处理进度</h3>
|
||||
{ task?.status?.value === TaskStatus.FAILED ?
|
||||
<Progress percent={task?.progress?.process} size="small" status="exception" />
|
||||
: <Progress percent={task?.progress?.process} size="small"/>
|
||||
}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
|
||||
<span>已完成: {task?.progress?.succeedFileNum || "0"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
|
||||
<span>处理中: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
|
||||
<span>失败: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
|
||||
import { Button, Card, Descriptions, Progress } from "antd";
|
||||
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {formatExecutionDuration} from "@/utils/unit.ts";
|
||||
|
||||
export default function BasicInfo({ task }: { task: CleansingTask }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const descriptionItems = [
|
||||
{
|
||||
key: "id",
|
||||
label: "任务ID",
|
||||
children: <span className="font-mono">{task?.id}</span>,
|
||||
},
|
||||
{ key: "name", label: "任务名称", children: task?.name },
|
||||
{
|
||||
key: "dataset",
|
||||
label: "源数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.srcDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.srcDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "targetDataset",
|
||||
label: "目标数据集",
|
||||
children: (
|
||||
<Button
|
||||
style={{ paddingLeft: 0, marginLeft: 0 }}
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
navigate("/data/management/detail/" + task?.destDatasetId)
|
||||
}
|
||||
>
|
||||
{task?.destDatasetName}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ key: "startTime", label: "开始时间", children: task?.startedAt },
|
||||
{
|
||||
key: "description",
|
||||
label: "任务描述",
|
||||
children: (
|
||||
<span className="text-gray-600">{task?.description || "--"}</span>
|
||||
),
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 执行摘要 */}
|
||||
<Card className="mb-6">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
|
||||
<Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-blue-500">
|
||||
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总耗时</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
|
||||
<CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-green-500">
|
||||
{task?.progress?.succeedFileNum || "0"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-red-500">
|
||||
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">失败文件</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
|
||||
<Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
|
||||
<div className="text-xl font-bold text-purple-500">
|
||||
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功率</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 基本信息 */}
|
||||
<Card>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
|
||||
<Descriptions
|
||||
column={2}
|
||||
bordered={false}
|
||||
size="middle"
|
||||
labelStyle={{ fontWeight: 500, color: "#555" }}
|
||||
contentStyle={{ fontSize: 14 }}
|
||||
items={descriptionItems}
|
||||
></Descriptions>
|
||||
</div>
|
||||
{/* 处理进度 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">处理进度</h3>
|
||||
{ task?.status?.value === TaskStatus.FAILED ?
|
||||
<Progress percent={task?.progress?.process} size="small" status="exception" />
|
||||
: <Progress percent={task?.progress?.process} size="small"/>
|
||||
}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
|
||||
<span>已完成: {task?.progress?.succeedFileNum || "0"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
|
||||
<span>处理中: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
|
||||
<span>失败: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
|
||||
task?.progress.failedFileNum :
|
||||
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,397 +1,397 @@
|
||||
import {Button, Modal, Table, Badge, Input, Popover} from "antd";
|
||||
import { Download } from "lucide-react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
|
||||
// 模拟文件列表数据
|
||||
export default function FileTable({result, fetchTaskResult}) {
|
||||
const { id = "" } = useParams();
|
||||
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskResult();
|
||||
}, [id]);
|
||||
|
||||
const handleSelectAllFiles = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds(result.map((file) => file.instanceId));
|
||||
} else {
|
||||
setSelectedFileIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds([...selectedFileIds, fileId]);
|
||||
} else {
|
||||
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
|
||||
}
|
||||
};
|
||||
const handleViewFileCompare = (file: any) => {
|
||||
setSelectedFile(file);
|
||||
setShowFileCompareDialog(true);
|
||||
};
|
||||
const handleBatchDownload = () => {
|
||||
// 实际下载逻辑
|
||||
};
|
||||
|
||||
function formatFileSize(bytes: number, decimals: number = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
selectedFileIds.length === result?.length && result?.length > 0
|
||||
}
|
||||
onChange={(e) => handleSelectAllFiles(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
dataIndex: "select",
|
||||
key: "select",
|
||||
width: 50,
|
||||
render: (_text: string, record: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFileIds.includes(record.id)}
|
||||
onChange={(e) => handleSelectFile(record.id, e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "srcName",
|
||||
key: "srcName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件名",
|
||||
dataIndex: "destName",
|
||||
key: "destName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件类型",
|
||||
dataIndex: "srcType",
|
||||
key: "srcType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件类型",
|
||||
dataIndex: "destType",
|
||||
key: "destType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗前大小",
|
||||
dataIndex: "srcSize",
|
||||
key: "srcSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后大小",
|
||||
dataIndex: "destSize",
|
||||
key: "destSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return (
|
||||
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
|
||||
);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
filters: [
|
||||
{ text: "已完成", value: "COMPLETED" },
|
||||
{ text: "失败", value: "FAILED" },
|
||||
],
|
||||
onFilter: (value: string, record: any) => record.status === value,
|
||||
render: (status: string) => (
|
||||
<Badge
|
||||
status={
|
||||
status === "COMPLETED"
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
text={TaskStatusMap[status as TaskStatus].label}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
render: (_text: string, record: any) => (
|
||||
<div className="flex">
|
||||
{record.status === "COMPLETED" ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleViewFileCompare(record)}
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
)}
|
||||
<Popover content="暂未开放">
|
||||
<Button type="link" size="small" disabled>下载</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedFileIds.length > 0 && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
已选择 {selectedFileIds.length} 个文件
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchDownload}
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Download className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
批量下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
columns={fileColumns}
|
||||
dataSource={result}
|
||||
pagination={{ pageSize: 10, showSizeChanger: true }}
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
{/* 文件对比弹窗 */}
|
||||
<Modal
|
||||
open={showFileCompareDialog}
|
||||
onCancel={() => setShowFileCompareDialog(false)}
|
||||
footer={null}
|
||||
width={900}
|
||||
title={<span>文件对比 - {selectedFile?.fileName}</span>}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6 py-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗前</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">原始文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.srcSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.srcType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗后</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">处理后文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.destSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.destType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 mt-6 pt-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">处理效果对比</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="font-medium text-green-700">文件大小优化</div>
|
||||
<div className="text-green-600">减少了 {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {Button, Modal, Table, Badge, Input, Popover} from "antd";
|
||||
import { Download } from "lucide-react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
|
||||
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
|
||||
|
||||
// 模拟文件列表数据
|
||||
export default function FileTable({result, fetchTaskResult}) {
|
||||
const { id = "" } = useParams();
|
||||
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskResult();
|
||||
}, [id]);
|
||||
|
||||
const handleSelectAllFiles = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds(result.map((file) => file.instanceId));
|
||||
} else {
|
||||
setSelectedFileIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFileIds([...selectedFileIds, fileId]);
|
||||
} else {
|
||||
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
|
||||
}
|
||||
};
|
||||
const handleViewFileCompare = (file: any) => {
|
||||
setSelectedFile(file);
|
||||
setShowFileCompareDialog(true);
|
||||
};
|
||||
const handleBatchDownload = () => {
|
||||
// 实际下载逻辑
|
||||
};
|
||||
|
||||
function formatFileSize(bytes: number, decimals: number = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
const fileColumns = [
|
||||
{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
selectedFileIds.length === result?.length && result?.length > 0
|
||||
}
|
||||
onChange={(e) => handleSelectAllFiles(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
dataIndex: "select",
|
||||
key: "select",
|
||||
width: 50,
|
||||
render: (_text: string, record: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFileIds.includes(record.id)}
|
||||
onChange={(e) => handleSelectFile(record.id, e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件名",
|
||||
dataIndex: "srcName",
|
||||
key: "srcName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件名",
|
||||
dataIndex: "destName",
|
||||
key: "destName",
|
||||
width: 200,
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件名"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destName.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "文件类型",
|
||||
dataIndex: "srcType",
|
||||
key: "srcType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.srcType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后文件类型",
|
||||
dataIndex: "destType",
|
||||
key: "destType",
|
||||
filterDropdown: ({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
clearFilters,
|
||||
}: any) => (
|
||||
<div className="p-4 w-64">
|
||||
<Input
|
||||
placeholder="搜索文件类型"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e) =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
onPressEnter={() => confirm()}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" onClick={() => confirm()}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearFilters()}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
onFilter: (value: string, record: any) =>
|
||||
record.destType.toLowerCase().includes(value.toLowerCase()),
|
||||
render: (text: string) => (
|
||||
<span className="font-mono text-sm">{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗前大小",
|
||||
dataIndex: "srcSize",
|
||||
key: "srcSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "清洗后大小",
|
||||
dataIndex: "destSize",
|
||||
key: "destSize",
|
||||
sorter: (a: any, b: any) => {
|
||||
const getSizeInBytes = (size: string) => {
|
||||
if (!size || size === "-") return 0;
|
||||
const num = Number.parseFloat(size);
|
||||
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
|
||||
if (size.includes("MB")) return num * 1024 * 1024;
|
||||
if (size.includes("KB")) return num * 1024;
|
||||
return num;
|
||||
};
|
||||
return (
|
||||
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
|
||||
);
|
||||
},
|
||||
render: (number: number) => (
|
||||
<span className="font-mono text-sm">{formatFileSize(number)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
filters: [
|
||||
{ text: "已完成", value: "COMPLETED" },
|
||||
{ text: "失败", value: "FAILED" },
|
||||
],
|
||||
onFilter: (value: string, record: any) => record.status === value,
|
||||
render: (status: string) => (
|
||||
<Badge
|
||||
status={
|
||||
status === "COMPLETED"
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
text={TaskStatusMap[status as TaskStatus].label}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
render: (_text: string, record: any) => (
|
||||
<div className="flex">
|
||||
{record.status === "COMPLETED" ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleViewFileCompare(record)}
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled
|
||||
>
|
||||
对比
|
||||
</Button>
|
||||
)}
|
||||
<Popover content="暂未开放">
|
||||
<Button type="link" size="small" disabled>下载</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedFileIds.length > 0 && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
已选择 {selectedFileIds.length} 个文件
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleBatchDownload}
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Download className="w-4 h-4 mr-2" />}
|
||||
>
|
||||
批量下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
columns={fileColumns}
|
||||
dataSource={result}
|
||||
pagination={{ pageSize: 10, showSizeChanger: true }}
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
{/* 文件对比弹窗 */}
|
||||
<Modal
|
||||
open={showFileCompareDialog}
|
||||
onCancel={() => setShowFileCompareDialog(false)}
|
||||
footer={null}
|
||||
width={900}
|
||||
title={<span>文件对比 - {selectedFile?.fileName}</span>}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6 py-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗前</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">原始文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.srcSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.srcType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">清洗后</h4>
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
|
||||
<div className="text-sm">处理后文件预览</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
大小: {formatFileSize(selectedFile?.destSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mt-3 space-y-1">
|
||||
<div>
|
||||
<span className="font-medium">文件格式:</span> {selectedFile?.destType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 mt-6 pt-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">处理效果对比</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="font-medium text-green-700">文件大小优化</div>
|
||||
<div className="text-green-600">减少了 {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import {useEffect} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {FileClock} from "lucide-react";
|
||||
|
||||
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
|
||||
const { id = "" } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskLog();
|
||||
}, [id]);
|
||||
|
||||
return taskLog?.length > 0 ? (
|
||||
<>
|
||||
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
|
||||
<div className="font-mono text-sm">
|
||||
{taskLog?.map?.((log, index) => (
|
||||
<div key={index} className="flex gap-3">
|
||||
<span
|
||||
className={`min-w-20 ${
|
||||
log.level === "ERROR" || log.level === "FATAL"
|
||||
? "text-red-500"
|
||||
: log.level === "WARNING" || log.level === "WARN"
|
||||
? "text-yellow-500"
|
||||
: "text-green-500"
|
||||
}`}
|
||||
>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="text-gray-100">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
当前任务无可用日志
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import {useEffect} from "react";
|
||||
import {useParams} from "react-router";
|
||||
import {FileClock} from "lucide-react";
|
||||
|
||||
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
|
||||
const { id = "" } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskLog();
|
||||
}, [id]);
|
||||
|
||||
return taskLog?.length > 0 ? (
|
||||
<>
|
||||
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
|
||||
<div className="font-mono text-sm">
|
||||
{taskLog?.map?.((log, index) => (
|
||||
<div key={index} className="flex gap-3">
|
||||
<span
|
||||
className={`min-w-20 ${
|
||||
log.level === "ERROR" || log.level === "FATAL"
|
||||
? "text-red-500"
|
||||
: log.level === "WARNING" || log.level === "WARN"
|
||||
? "text-yellow-500"
|
||||
: "text-green-500"
|
||||
}`}
|
||||
>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="text-gray-100">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
当前任务无可用日志
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import {Steps, Typography} from "antd";
|
||||
import {useNavigate} from "react-router";
|
||||
|
||||
export default function OperatorTable({ task }: { task: any }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return task?.instance?.length > 0 && (
|
||||
<>
|
||||
<Steps
|
||||
progressDot
|
||||
direction="vertical"
|
||||
items={Object.values(task?.instance).map((item) => ({
|
||||
title: <Typography.Link
|
||||
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
|
||||
>
|
||||
{item?.name}
|
||||
</Typography.Link>,
|
||||
description: item?.description,
|
||||
status: "finish"
|
||||
}))}
|
||||
className="overflow-auto"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import {Steps, Typography} from "antd";
|
||||
import {useNavigate} from "react-router";
|
||||
|
||||
export default function OperatorTable({ task }: { task: any }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return task?.instance?.length > 0 && (
|
||||
<>
|
||||
<Steps
|
||||
progressDot
|
||||
direction="vertical"
|
||||
items={Object.values(task?.instance).map((item) => ({
|
||||
title: <Typography.Link
|
||||
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
|
||||
>
|
||||
{item?.name}
|
||||
</Typography.Link>,
|
||||
description: item?.description,
|
||||
status: "finish"
|
||||
}))}
|
||||
className="overflow-auto"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user