feat: fix the problem in the Operator Market frontend pages

This commit is contained in:
root
2025-12-29 11:38:47 +08:00
parent 29e4a333a9
commit 844add27ea
213 changed files with 45547 additions and 45537 deletions

View File

@@ -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>
</>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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"
/>
</>
);
}