feature: 清洗任务详情页 (#73)

* feature: 清洗任务详情

* fix: 取消构建镜像,改为直接拉取

* fix: 增加清洗任务详情页

* fix: 增加清洗任务详情页

* fix: 算子列表可点击

* fix: 模板详情和更新
This commit is contained in:
hhhhsc701
2025-11-12 18:00:19 +08:00
committed by GitHub
parent 442e561817
commit 6bbde0ec56
46 changed files with 1065 additions and 795 deletions

View File

@@ -1,29 +1,30 @@
import { useEffect, useState } from "react";
import { Card, Breadcrumb, App } from "antd";
import {Breadcrumb, App, Tabs} from "antd";
import {
Play,
Pause,
Clock,
CheckCircle,
AlertCircle,
Database,
Trash2,
Activity,
Activity, LayoutList,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost,
queryCleaningTaskByIdUsingGet,
queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet,
stopCleaningTaskUsingPost,
} from "../cleansing.api";
import { TaskStatusMap } from "../cleansing.const";
import { TaskStatus } from "@/pages/DataCleansing/cleansing.model";
import {mapTask, TaskStatusMap} from "../cleansing.const";
import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import BasicInfo from "./components/BasicInfo";
import OperatorTable from "./components/OperatorTable";
import FileTable from "./components/FileTable";
import LogsTable from "./components/LogsTable";
import {formatExecutionDuration} from "@/utils/unit.ts";
import {ReloadOutlined} from "@ant-design/icons";
// 任务详情页面组件
export default function CleansingTaskDetail() {
@@ -35,7 +36,7 @@ export default function CleansingTaskDetail() {
if (!id) return;
try {
const { data } = await queryCleaningTaskByIdUsingGet(id);
setTask(data);
setTask(mapTask(data));
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
@@ -60,6 +61,38 @@ export default function CleansingTaskDetail() {
navigate("/data/cleansing");
};
const [result, setResult] = useState<CleansingResult[]>();
const fetchTaskResult = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskResultByIdUsingGet(id);
setResult(data);
} catch (error) {
message.error("获取清洗结果失败");
navigate("/data/cleansing/task-detail/" + id);
}
};
const [taskLog, setTaskLog] = useState();
const fetchTaskLog = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskLogByIdUsingGet(id);
setTaskLog(data);
} catch (error) {
message.error("获取清洗日志失败");
navigate("/data/cleansing/task-detail/" + id);
}
};
const handleRefresh = async () => {
fetchTaskDetail();
{activeTab === "files" && await fetchTaskResult()}
{activeTab === "logs" && await fetchTaskLog()}
};
useEffect(() => {
fetchTaskDetail();
}, [id]);
@@ -69,9 +102,9 @@ export default function CleansingTaskDetail() {
const headerData = {
...task,
icon: <Database className="w-8 h-8" />,
icon: <LayoutList className="w-8 h-8" />,
status: TaskStatusMap[task?.status],
createdAt: task?.startTime,
createdAt: task?.createdAt,
lastUpdated: task?.updatedAt,
};
@@ -79,22 +112,24 @@ export default function CleansingTaskDetail() {
{
icon: <Clock className="w-4 h-4 text-blue-500" />,
label: "总耗时",
value: task?.duration || "--",
value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--",
},
{
icon: <CheckCircle className="w-4 h-4 text-green-500" />,
label: "成功文件",
value: task?.successFiles || "--",
value: task?.progress?.succeedFileNum || "0",
},
{
icon: <AlertCircle className="w-4 h-4 text-red-500" />,
label: "失败文件",
value: task?.failedFiles || "--",
value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum,
},
{
icon: <Activity className="w-4 h-4 text-purple-500" />,
label: "成功率",
value: `${task?.progress}%`,
value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--",
},
];
@@ -109,7 +144,7 @@ export default function CleansingTaskDetail() {
},
]
: []),
...(task?.status === TaskStatus.PENDING
...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value)
? [
{
key: "start",
@@ -119,6 +154,12 @@ export default function CleansingTaskDetail() {
},
]
: []),
{
key: "refresh",
label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除任务",
@@ -131,20 +172,20 @@ export default function CleansingTaskDetail() {
const tabList = [
{
key: "basic",
tab: "基本信息",
children: <BasicInfo task={task} />,
label: "基本信息",
},
{
key: "operators",
tab: "处理算子",
children: <OperatorTable task={task} />,
label: "处理算子",
},
{
key: "files",
tab: "处理文件",
children: <FileTable task={task} />,
label: "处理文件",
},
{
key: "logs",
label: "运行日志",
},
{ key: "logs", tab: "运行日志", children: <LogsTable task={task} /> },
];
const breadItems = [
@@ -157,7 +198,7 @@ export default function CleansingTaskDetail() {
];
return (
<div className="min-h-screen">
<>
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
@@ -166,11 +207,17 @@ export default function CleansingTaskDetail() {
operations={operations}
/>
</div>
<Card
tabList={tabList}
activeTabKey={activeTab}
onTabChange={setActiveTab}
></Card>
</div>
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full flex-1 overflow-auto">
{activeTab === "basic" && (
<BasicInfo task={task} />
)}
{activeTab === "operators" && <OperatorTable task={task} />}
{activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />}
{activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,122 @@
import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd";
import {
Trash2,
LayoutList,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTemplateByIdUsingDelete,
queryCleaningTemplateByIdUsingGet,
} from "../cleansing.api";
import {mapTemplate} from "../cleansing.const";
import OperatorTable from "./components/OperatorTable";
import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons";
// 任务详情页面组件
export default function CleansingTemplateDetail() {
const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp();
const navigate = useNavigate();
const [template, setTemplate] = useState();
const fetchTemplateDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplate(mapTemplate(data));
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
const deleteTemplate = async () => {
await deleteCleaningTemplateByIdUsingDelete(id);
message.success("模板已删除");
navigate("/data/cleansing");
};
const handleRefresh = async () => {
fetchTemplateDetail();
};
useEffect(() => {
fetchTemplateDetail();
}, [id]);
const [activeTab, setActiveTab] = useState("operators");
const headerData = {
...template,
icon: <LayoutList className="w-8 h-8" />,
createdAt: template?.createdAt,
lastUpdated: template?.updatedAt,
};
const statistics = [
{
icon: <NumberOutlined className="w-4 h-4 text-green-500" />,
label: "算子数量",
value: template?.instance?.length || 0,
},
];
const operations = [
{
key: "update",
label: "更新任务",
icon: <EditOutlined className="w-4 h-4" />,
onClick: () => navigate(`/data/cleansing/update-template/${id}`),
},
{
key: "refresh",
label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除任务",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: deleteTemplate,
},
];
const tabList = [
{
key: "operators",
label: "处理算子",
},
];
const breadItems = [
{
title: <Link to="/data/cleansing"></Link>,
},
{
title: "模板详情",
},
];
return (
<>
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
data={headerData}
statistics={statistics}
operations={operations}
/>
</div>
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full flex-1 overflow-auto">
<OperatorTable task={template} />
</div>
</div>
</>
);
}

View File

@@ -1,8 +1,8 @@
import type { CleansingTask } from "@/pages/DataCleansing/cleansing.model";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import { Button, Card, Descriptions, Progress, Tag } from "antd";
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();
@@ -11,7 +11,7 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
{
key: "id",
label: "任务ID",
children: <span className="font-mono">#{task?.id}</span>,
children: <span className="font-mono">{task?.id}</span>,
},
{ key: "name", label: "任务名称", children: task?.name },
{
@@ -19,6 +19,7 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
label: "源数据集",
children: (
<Button
style={{ paddingLeft: 0, marginLeft: 0 }}
type="link"
size="small"
onClick={() =>
@@ -34,6 +35,7 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
label: "目标数据集",
children: (
<Button
style={{ paddingLeft: 0, marginLeft: 0 }}
type="link"
size="small"
onClick={() =>
@@ -44,26 +46,12 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
</Button>
),
},
{ key: "template", label: "使用模板", children: task?.template },
{ key: "startTime", label: "开始时间", children: task?.startedAt },
{ key: "estimatedTime", label: "预计用时", children: task?.estimatedTime },
{
key: "description",
label: "任务描述",
children: (
<span className="text-gray-600">{task?.description || "暂无描述"}</span>
),
span: 2,
},
{
key: "rules",
label: "处理算子",
children: (
<div className="flex flex-wrap gap-1">
{task?.instance?.map?.((op: OperatorI) => (
<Tag key={op.id}>{op.name}</Tag>
))}
</div>
<span className="text-gray-600">{task?.description || "--"}</span>
),
span: 2,
},
@@ -77,28 +65,30 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
<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">
{task?.duration || "--"}
{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?.successFiles || "--"}
{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?.failedFiles || "--"}
{(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 || "--"}
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
@@ -120,25 +110,22 @@ export default function BasicInfo({ task }: { task: CleansingTask }) {
{/* 处理进度 */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<Progress percent={task?.progress} showInfo />
<Progress percent={task?.progress?.process} showInfo />
<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?.processedFiles || "--"}</span>
<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?.processingFiles || "--"}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-gray-300 rounded-full inline-block" />
<span>
: {task?.totalFiles - task?.processedFiles || "--"}
</span>
<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?.failedFiles || "--"}</span>
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
</div>
</div>
</div>

View File

@@ -1,70 +1,30 @@
import { Button, Modal, Table, Badge, Input } from "antd";
import { Download, FileText } from "lucide-react";
import { useState } from "react";
import {Button, Modal, Table, Badge, Input} 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";
// 模拟文件列表数据
const fileList = [
{
id: 1,
fileName: "lung_cancer_001.svs",
originalSize: "15.2MB",
processedSize: "8.5MB",
status: "已完成",
duration: "2分15秒",
processedAt: "2024-01-20 09:32:40",
},
{
id: 2,
fileName: "lung_cancer_002.svs",
originalSize: "18.7MB",
processedSize: "10.2MB",
status: "已完成",
duration: "2分38秒",
processedAt: "2024-01-20 09:35:18",
},
{
id: 3,
fileName: "lung_cancer_003.svs",
originalSize: "12.3MB",
processedSize: "6.8MB",
status: "已完成",
duration: "1分52秒",
processedAt: "2024-01-20 09:37:10",
},
{
id: 4,
fileName: "lung_cancer_004.svs",
originalSize: "20.1MB",
processedSize: "-",
status: "失败",
duration: "0分45秒",
processedAt: "2024-01-20 09:38:55",
},
{
id: 5,
fileName: "lung_cancer_005.svs",
originalSize: "16.8MB",
processedSize: "9.3MB",
status: "已完成",
duration: "2分22秒",
processedAt: "2024-01-20 09:41:17",
},
];
export default function FileTable() {
export default function FileTable({result, fetchTaskResult}) {
const { id = "" } = useParams();
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
const [showFileLogDialog, setShowFileLogDialog] = useState(false);
const [selectedFile, setSelectedFile] = useState<any>(null);
const [selectedFileIds, setSelectedFileIds] = useState<number[]>([]);
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
useEffect(() => {
fetchTaskResult();
}, [id]);
const handleSelectAllFiles = (checked: boolean) => {
if (checked) {
setSelectedFileIds(fileList.map((file) => file.id));
setSelectedFileIds(result.map((file) => file.instanceId));
} else {
setSelectedFileIds([]);
}
};
const handleSelectFile = (fileId: number, checked: boolean) => {
const handleSelectFile = (fileId: string, checked: boolean) => {
if (checked) {
setSelectedFileIds([...selectedFileIds, fileId]);
} else {
@@ -79,116 +39,16 @@ export default function FileTable() {
// 实际下载逻辑
};
const handleBatchDeleteFiles = () => {
// 实际删除逻辑
setSelectedFileIds([]);
};
const handleViewFileLog = (file: any) => {
setSelectedFile(file);
setShowFileLogDialog(true);
};
function formatFileSize(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
// 模拟单个文件的处理日志
const getFileProcessLog = (fileName: string) => [
{
time: "09:30:18",
step: "开始处理",
operator: "格式转换",
status: "INFO",
message: `开始处理文件: ${fileName}`,
},
{
time: "09:30:19",
step: "文件验证",
operator: "格式转换",
status: "INFO",
message: "验证文件格式和完整性",
},
{
time: "09:30:20",
step: "格式解析",
operator: "格式转换",
status: "INFO",
message: "解析SVS格式文件",
},
{
time: "09:30:25",
step: "格式转换",
operator: "格式转换",
status: "SUCCESS",
message: "成功转换为JPEG格式",
},
{
time: "09:30:26",
step: "噪声检测",
operator: "噪声去除",
status: "INFO",
message: "检测图像噪声水平",
},
{
time: "09:30:28",
step: "噪声去除",
operator: "噪声去除",
status: "INFO",
message: "应用高斯滤波去除噪声",
},
{
time: "09:30:31",
step: "噪声去除完成",
operator: "噪声去除",
status: "SUCCESS",
message: "噪声去除处理完成",
},
{
time: "09:30:32",
step: "尺寸检测",
operator: "尺寸标准化",
status: "INFO",
message: "检测当前图像尺寸: 2048x1536",
},
{
time: "09:30:33",
step: "尺寸调整",
operator: "尺寸标准化",
status: "INFO",
message: "调整图像尺寸至512x512",
},
{
time: "09:30:35",
step: "尺寸标准化完成",
operator: "尺寸标准化",
status: "SUCCESS",
message: "图像尺寸标准化完成",
},
{
time: "09:30:36",
step: "质量检查",
operator: "质量检查",
status: "INFO",
message: "检查图像质量指标",
},
{
time: "09:30:38",
step: "分辨率检查",
operator: "质量检查",
status: "SUCCESS",
message: "分辨率符合要求",
},
{
time: "09:30:39",
step: "清晰度检查",
operator: "质量检查",
status: "SUCCESS",
message: "图像清晰度良好",
},
{
time: "09:30:40",
step: "处理完成",
operator: "质量检查",
status: "SUCCESS",
message: `文件 ${fileName} 处理完成`,
},
];
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 = [
{
@@ -196,7 +56,7 @@ export default function FileTable() {
<input
type="checkbox"
checked={
selectedFileIds.length === fileList.length && fileList.length > 0
selectedFileIds.length === result?.length && result?.length > 0
}
onChange={(e) => handleSelectAllFiles(e.target.checked)}
className="w-4 h-4"
@@ -205,7 +65,7 @@ export default function FileTable() {
dataIndex: "select",
key: "select",
width: 50,
render: (text: string, record: any) => (
render: (_text: string, record: any) => (
<input
type="checkbox"
checked={selectedFileIds.includes(record.id)}
@@ -216,8 +76,8 @@ export default function FileTable() {
},
{
title: "文件名",
dataIndex: "fileName",
key: "fileName",
dataIndex: "srcName",
key: "srcName",
filterDropdown: ({
setSelectedKeys,
selectedKeys,
@@ -245,15 +105,87 @@ export default function FileTable() {
</div>
),
onFilter: (value: string, record: any) =>
record.fileName.toLowerCase().includes(value.toLowerCase()),
record.srcName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span className="font-mono text-sm">{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: "originalSize",
key: "originalSize",
dataIndex: "srcSize",
key: "srcSize",
sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0;
@@ -265,11 +197,14 @@ export default function FileTable() {
};
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
},
render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span>
),
},
{
title: "清洗后大小",
dataIndex: "processedSize",
key: "processedSize",
dataIndex: "destSize",
key: "destSize",
sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0;
@@ -283,6 +218,9 @@ export default function FileTable() {
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
);
},
render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span>
),
},
{
title: "状态",
@@ -297,43 +235,22 @@ export default function FileTable() {
render: (status: string) => (
<Badge
status={
status === "已完成"
status === "COMPLETED"
? "success"
: status === "失败"
: status === "FAILED"
? "error"
: "processing"
}
text={status}
text={TaskStatusMap[status as TaskStatus].label}
/>
),
},
{
title: "执行耗时",
dataIndex: "duration",
key: "duration",
sorter: (a: any, b: any) => {
const getTimeInSeconds = (duration: string) => {
const parts = duration.split(/[分秒]/);
const minutes = Number.parseInt(parts[0]) || 0;
const seconds = Number.parseInt(parts[1]) || 0;
return minutes * 60 + seconds;
};
return getTimeInSeconds(a.duration) - getTimeInSeconds(b.duration);
},
},
{
title: "操作",
key: "action",
render: (text: string, record: any) => (
render: (_text: string, record: any) => (
<div className="flex">
<Button
type="link"
size="small"
onClick={() => handleViewFileLog(record)}
>
</Button>
{record.status === "已完成" && (
{record.status === "COMPLETED" && (
<Button
type="link"
size="small"
@@ -371,53 +288,12 @@ export default function FileTable() {
)}
<Table
columns={fileColumns}
dataSource={fileList}
dataSource={result}
pagination={{ pageSize: 10, showSizeChanger: true }}
size="middle"
rowKey="id"
/>
{/* 文件日志弹窗 */}
<Modal
open={showFileLogDialog}
onCancel={() => setShowFileLogDialog(false)}
footer={null}
width={700}
title={
<span>
<FileText className="w-4 h-4 mr-2 inline" />
- {selectedFile?.fileName}
</span>
}
>
<div className="py-4">
<div className="bg-gray-900 rounded-lg p-4 max-h-96 overflow-y-auto">
<div className="font-mono text-sm">
{selectedFile &&
getFileProcessLog(selectedFile.fileName).map((log, index) => (
<div key={index} className="flex gap-3">
<span className="text-gray-500 min-w-20">{log.time}</span>
<span className="text-blue-400 min-w-24">
[{log.operator}]
</span>
<span
className={`min-w-20 ${
log.status === "ERROR"
? "text-red-400"
: log.status === "SUCCESS"
? "text-green-400"
: "text-yellow-400"
}`}
>
{log.step}
</span>
<span className="text-gray-100">{log.message}</span>
</div>
))}
</div>
</div>
</div>
</Modal>
{/* 文件对比弹窗 */}
<Modal
open={showFileCompareDialog}
@@ -434,22 +310,13 @@ export default function FileTable() {
<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">
: {selectedFile?.originalSize}
: {formatFileSize(selectedFile?.srcSize)}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> SVS
</div>
<div>
<span className="font-medium">:</span> 2048x1536
</div>
<div>
<span className="font-medium">:</span> RGB
</div>
<div>
<span className="font-medium">:</span>
<span className="font-medium">:</span> {selectedFile?.srcType}
</div>
</div>
</div>
@@ -460,22 +327,13 @@ export default function FileTable() {
<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">
: {selectedFile?.processedSize}
: {formatFileSize(selectedFile?.destSize)}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> JPEG
</div>
<div>
<span className="font-medium">:</span> 512x512
</div>
<div>
<span className="font-medium">:</span> RGB
</div>
<div>
<span className="font-medium">:</span> JPEG压缩
<span className="font-medium">:</span> {selectedFile?.destType}
</div>
</div>
</div>
@@ -485,15 +343,7 @@ export default function FileTable() {
<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"> 44.1%</div>
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<div className="font-medium text-blue-700"></div>
<div className="text-blue-600">{selectedFile?.duration}</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="font-medium text-purple-700"></div>
<div className="text-purple-600"> (9.2/10)</div>
<div className="text-green-600"> {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
</div>
</div>
</div>

View File

@@ -1,110 +1,43 @@
export default function LogsTable({ task }: { task: any }) {
// 模拟运行日志
const runLogs = [
{
time: "09:30:15",
level: "INFO",
message: "开始执行数据清洗任务: 肺癌WSI图像清洗任务",
},
{
time: "09:30:16",
level: "INFO",
message: "加载源数据集: 肺癌WSI病理图像数据集 (1250 文件)",
},
{ time: "09:30:17", level: "INFO", message: "初始化算子: 格式转换" },
{
time: "09:30:18",
level: "INFO",
message: "开始处理文件: lung_cancer_001.svs",
},
{
time: "09:30:25",
level: "SUCCESS",
message: "文件处理成功: lung_cancer_001.svs -> lung_cancer_001.jpg",
},
{
time: "09:30:26",
level: "INFO",
message: "开始处理文件: lung_cancer_002.svs",
},
{
time: "09:30:33",
level: "SUCCESS",
message: "文件处理成功: lung_cancer_002.svs -> lung_cancer_002.jpg",
},
{
time: "09:58:42",
level: "INFO",
message: "格式转换完成,成功处理 1250/1250 文件",
},
{ time: "09:58:43", level: "INFO", message: "初始化算子: 噪声去除" },
{
time: "09:58:44",
level: "INFO",
message: "开始处理文件: lung_cancer_001.jpg",
},
{
time: "09:58:51",
level: "SUCCESS",
message: "噪声去除成功: lung_cancer_001.jpg",
},
{
time: "10:15:23",
level: "WARNING",
message: "文件质量较低,跳过处理: lung_cancer_156.jpg",
},
{
time: "10:35:18",
level: "INFO",
message: "噪声去除完成,成功处理 1228/1250 文件",
},
{ time: "10:35:19", level: "INFO", message: "初始化算子: 尺寸标准化" },
{
time: "11:12:05",
level: "INFO",
message: "尺寸标准化完成,成功处理 1222/1228 文件",
},
{ time: "11:12:06", level: "INFO", message: "初始化算子: 质量检查" },
{
time: "11:25:33",
level: "ERROR",
message: "质量检查失败: lung_cancer_089.jpg - 分辨率过低",
},
{
time: "11:45:32",
level: "INFO",
message: "质量检查完成,成功处理 1198/1222 文件",
},
{
time: "11:45:33",
level: "SUCCESS",
message: "数据清洗任务完成!总成功率: 95.8%",
},
];
import {useEffect} from "react";
import {useParams} from "react-router";
import {FileClock} from "lucide-react";
return (
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
<div className="font-mono text-sm">
{runLogs?.map?.((log, index) => (
<div key={index} className="flex gap-3">
<span className="text-gray-500 min-w-20">{log.time}</span>
<span
className={`min-w-20 ${
log.level === "ERROR"
? "text-red-500"
: log.level === "WARNING"
? "text-yellow-500"
: log.level === "SUCCESS"
? "text-green-500"
: "text-blue-500"
}`}
>
[{log.level}]
</span>
<span className="text-gray-100">{log.message}</span>
</div>
))}
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,103 +1,25 @@
import { Button, Input, Table } from "antd";
import {Steps, Typography} from "antd";
import {useNavigate} from "react-router";
const operators = [
{
name: "格式转换",
startTime: "09:30:15",
endTime: "09:58:42",
duration: "28分27秒",
status: "成功",
processedFiles: 1250,
successRate: 100,
},
{
name: "噪声去除",
startTime: "09:58:42",
endTime: "10:35:18",
duration: "36分36秒",
status: "成功",
processedFiles: 1250,
successRate: 98.2,
},
{
name: "尺寸标准化",
startTime: "10:35:18",
endTime: "11:12:05",
duration: "36分47秒",
status: "成功",
processedFiles: 1228,
successRate: 99.5,
},
{
name: "质量检查",
startTime: "11:12:05",
endTime: "11:45:32",
duration: "33分27秒",
status: "成功",
processedFiles: 1222,
successRate: 97.8,
},
];
export default function OperatorTable({ task }: { task: any }) {
const operatorColumns = [
{
title: "算子名称",
dataIndex: "name",
key: "name",
fixed: "left",
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.name.toLowerCase().includes(value.toLowerCase()),
},
{
title: "版本",
dataIndex: "version",
key: "version",
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
},
{
title: "更新时间",
dataIndex: "updatedAt",
key: "updatedAt",
},
];
const navigate = useNavigate();
return (
<Table
columns={operatorColumns}
dataSource={task?.instance || operators}
pagination={false}
size="middle"
/>
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"
/>
</>
);
}