You've already forked DataMate
feat: enhance useFetchData hook with polling functionality and improve task progress tracking
This commit is contained in:
@@ -37,6 +37,7 @@ interface CardViewProps<T> {
|
|||||||
| {
|
| {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
danger?: boolean;
|
||||||
icon?: React.JSX.Element;
|
icon?: React.JSX.Element;
|
||||||
onClick?: (item: T) => void;
|
onClick?: (item: T) => void;
|
||||||
}[]
|
}[]
|
||||||
@@ -169,82 +170,85 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
|||||||
typeof operations === "function" ? operations(item) : operations;
|
typeof operations === "function" ? operations(item) : operations;
|
||||||
return (
|
return (
|
||||||
<div className="flex-overflow-hidden">
|
<div className="flex-overflow-hidden">
|
||||||
<div className="flex-overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
<div className="overflow-auto grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||||
{data.map((item) => (
|
{data.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="border-card p-4 bg-white hover:shadow-lg transition-shadow duration-200"
|
className="border-card p-4 bg-white hover:shadow-lg transition-shadow duration-200"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col space-y-4 h-full">
|
<div className="flex flex-col space-y-4 h-full">
|
||||||
{/* Header */}
|
<div
|
||||||
<div className="flex items-start justify-between">
|
className="flex flex-col space-y-4 h-full"
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
onClick={() => onView?.(item)}
|
||||||
{item?.icon && (
|
style={{ cursor: onView ? "pointer" : "default" }}
|
||||||
<div
|
>
|
||||||
className={`flex-shrink-0 w-12 h-12 ${
|
{/* Header */}
|
||||||
item?.iconColor ||
|
<div className="flex items-start justify-between">
|
||||||
"bg-gradient-to-br from-blue-100 to-blue-200"
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
} rounded-lg flex items-center justify-center`}
|
{item?.icon && (
|
||||||
>
|
<div
|
||||||
{item?.icon}
|
className={`flex-shrink-0 w-12 h-12 ${
|
||||||
</div>
|
item?.iconColor ||
|
||||||
)}
|
"bg-gradient-to-br from-blue-100 to-blue-200"
|
||||||
<div className="flex-1 min-w-0">
|
} rounded-lg flex items-center justify-center`}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h3
|
|
||||||
className={`text-base flex-1 text-ellipsis overflow-hidden whitespace-nowrap font-semibold text-gray-900 truncate ${
|
|
||||||
onView ? "cursor-pointer hover:text-blue-600" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => onView?.(item)}
|
|
||||||
>
|
>
|
||||||
{item?.name}
|
{item?.icon}
|
||||||
</h3>
|
</div>
|
||||||
{item?.status && (
|
)}
|
||||||
<Tag color={item?.status?.color}>
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 text-xs py-0.5">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span>{item?.status?.icon}</span>
|
<h3
|
||||||
<span>{item?.status?.label}</span>
|
className={`text-base flex-1 text-ellipsis overflow-hidden whitespace-nowrap font-semibold text-gray-900 truncate`}
|
||||||
</div>
|
>
|
||||||
</Tag>
|
{item?.name}
|
||||||
)}
|
</h3>
|
||||||
|
{item?.status && (
|
||||||
|
<Tag color={item?.status?.color}>
|
||||||
|
<div className="flex items-center gap-2 text-xs py-0.5">
|
||||||
|
<span>{item?.status?.icon}</span>
|
||||||
|
<span>{item?.status?.label}</span>
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{onFavorite && (
|
||||||
|
<StarFilled
|
||||||
|
style={{
|
||||||
|
fontSize: "16px",
|
||||||
|
color: isFavorite?.(item) ? "#ffcc00ff" : "#d1d5db",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => onFavorite?.(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{onFavorite && (
|
|
||||||
<StarFilled
|
|
||||||
style={{
|
|
||||||
fontSize: "16px",
|
|
||||||
color: isFavorite?.(item) ? "#ffcc00ff" : "#d1d5db",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={() => onFavorite?.(item)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col justify-end">
|
<div className="flex-1 flex flex-col justify-end">
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<TagsRenderer tags={item?.tags || []} />
|
<TagsRenderer tags={item?.tags || []} />
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2 mt-2">
|
<p className="text-gray-600 text-xs text-ellipsis overflow-hidden whitespace-nowrap text-xs line-clamp-2 mt-2">
|
||||||
<Tooltip title={item?.description}>
|
<Tooltip title={item?.description}>
|
||||||
{item?.description}
|
{item?.description}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Statistics */}
|
{/* Statistics */}
|
||||||
<div className="grid grid-cols-2 gap-4 py-3">
|
<div className="grid grid-cols-2 gap-4 py-3">
|
||||||
{item?.statistics?.map((stat, idx) => (
|
{item?.statistics?.map((stat, idx) => (
|
||||||
<div key={idx}>
|
<div key={idx}>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{stat?.label}:
|
{stat?.label}:
|
||||||
|
</div>
|
||||||
|
<div className="text-base font-semibold text-gray-900">
|
||||||
|
{stat?.value}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base font-semibold text-gray-900">
|
))}
|
||||||
{stat?.value}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,32 @@
|
|||||||
// 首页数据获取
|
// 首页数据获取
|
||||||
import { useState } from "react";
|
// 支持轮询功能,使用示例:
|
||||||
|
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
|
||||||
|
// fetchFunction,
|
||||||
|
// mapFunction,
|
||||||
|
// 5000 // 5秒轮询一次,默认30秒
|
||||||
|
// false // 是否自动开始轮询,默认 true
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// startPolling(); // 开始轮询
|
||||||
|
// stopPolling(); // 停止轮询
|
||||||
|
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
|
||||||
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { useDebouncedEffect } from "./useDebouncedEffect";
|
import { useDebouncedEffect } from "./useDebouncedEffect";
|
||||||
import Loading from "@/utils/loading";
|
import Loading from "@/utils/loading";
|
||||||
import { App } from "antd";
|
import { App } from "antd";
|
||||||
|
|
||||||
export default function useFetchData<T>(
|
export default function useFetchData<T>(
|
||||||
fetchFunc: (params?: any) => Promise<any>,
|
fetchFunc: (params?: any) => Promise<any>,
|
||||||
mapDataFunc: (data: any) => T = (data) => data as T
|
mapDataFunc: (data: any) => T = (data) => data as T,
|
||||||
|
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||||
|
autoRefresh: boolean = true
|
||||||
) {
|
) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
// 轮询相关状态
|
||||||
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// 表格数据
|
// 表格数据
|
||||||
const [tableData, setTableData] = useState<T[]>([]);
|
const [tableData, setTableData] = useState<T[]>([]);
|
||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
@@ -55,39 +73,108 @@ export default function useFetchData<T>(
|
|||||||
return arr[0];
|
return arr[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(extraParams = {}) {
|
// 清除轮询定时器
|
||||||
const { keyword, filter, current, pageSize } = searchParams;
|
const clearPollingTimer = useCallback(() => {
|
||||||
Loading.show();
|
if (pollingTimerRef.current) {
|
||||||
setLoading(true);
|
clearTimeout(pollingTimerRef.current);
|
||||||
try {
|
pollingTimerRef.current = null;
|
||||||
const { data } = await fetchFunc({
|
|
||||||
...filter,
|
|
||||||
...extraParams,
|
|
||||||
keyword,
|
|
||||||
type: getFirstOfArray(filter?.type) || undefined,
|
|
||||||
status: getFirstOfArray(filter?.status) || undefined,
|
|
||||||
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
|
||||||
page: current - 1,
|
|
||||||
size: pageSize,
|
|
||||||
});
|
|
||||||
setPagination((prev) => ({
|
|
||||||
...prev,
|
|
||||||
total: data?.totalElements || 0,
|
|
||||||
}));
|
|
||||||
let result = [];
|
|
||||||
if (mapDataFunc) {
|
|
||||||
result = data?.content.map(mapDataFunc) ?? [];
|
|
||||||
}
|
|
||||||
setTableData(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
message.error("数据获取失败,请稍后重试");
|
|
||||||
} finally {
|
|
||||||
Loading.hide();
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = useCallback(
|
||||||
|
async (extraParams = {}, skipPollingRestart = false) => {
|
||||||
|
const { keyword, filter, current, pageSize } = searchParams;
|
||||||
|
|
||||||
|
if (!skipPollingRestart) {
|
||||||
|
Loading.show();
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在轮询且不是轮询触发的调用,先停止当前轮询
|
||||||
|
const wasPolling = isPolling && !skipPollingRestart;
|
||||||
|
if (wasPolling) {
|
||||||
|
clearPollingTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await fetchFunc({
|
||||||
|
...filter,
|
||||||
|
...extraParams,
|
||||||
|
keyword,
|
||||||
|
type: getFirstOfArray(filter?.type) || undefined,
|
||||||
|
status: getFirstOfArray(filter?.status) || undefined,
|
||||||
|
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
||||||
|
page: current - 1,
|
||||||
|
size: pageSize,
|
||||||
|
});
|
||||||
|
setPagination((prev) => ({
|
||||||
|
...prev,
|
||||||
|
total: data?.totalElements || 0,
|
||||||
|
}));
|
||||||
|
let result = [];
|
||||||
|
if (mapDataFunc) {
|
||||||
|
result = data?.content.map(mapDataFunc) ?? [];
|
||||||
|
}
|
||||||
|
setTableData(result);
|
||||||
|
|
||||||
|
// 如果之前正在轮询且不是轮询触发的调用,重新开始轮询
|
||||||
|
if (wasPolling) {
|
||||||
|
const poll = () => {
|
||||||
|
pollingTimerRef.current = setTimeout(() => {
|
||||||
|
fetchData({}, true).then(() => {
|
||||||
|
if (pollingTimerRef.current) {
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, pollingInterval);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
message.error("数据获取失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
Loading.hide();
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
searchParams,
|
||||||
|
fetchFunc,
|
||||||
|
mapDataFunc,
|
||||||
|
isPolling,
|
||||||
|
clearPollingTimer,
|
||||||
|
pollingInterval,
|
||||||
|
message,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
const startPolling = useCallback(() => {
|
||||||
|
clearPollingTimer();
|
||||||
|
setIsPolling(true);
|
||||||
|
|
||||||
|
const poll = () => {
|
||||||
|
pollingTimerRef.current = setTimeout(() => {
|
||||||
|
fetchData({}, true).then(() => {
|
||||||
|
if (pollingTimerRef.current) {
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, pollingInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
|
}, [pollingInterval, clearPollingTimer, fetchData]);
|
||||||
|
|
||||||
|
// 停止轮询
|
||||||
|
const stopPolling = useCallback(() => {
|
||||||
|
clearPollingTimer();
|
||||||
|
setIsPolling(false);
|
||||||
|
}, [clearPollingTimer]);
|
||||||
|
|
||||||
|
// 搜索参数变化时,自动刷新数据
|
||||||
|
// keyword 变化时,防抖500ms后刷新
|
||||||
useDebouncedEffect(
|
useDebouncedEffect(
|
||||||
() => {
|
() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -96,6 +183,16 @@ export default function useFetchData<T>(
|
|||||||
searchParams?.keyword ? 500 : 0
|
searchParams?.keyword ? 500 : 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 组件卸载时清理轮询
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoRefresh) {
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
clearPollingTimer();
|
||||||
|
};
|
||||||
|
}, [clearPollingTimer]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
tableData,
|
tableData,
|
||||||
@@ -109,5 +206,8 @@ export default function useFetchData<T>(
|
|||||||
setPagination,
|
setPagination,
|
||||||
handleFiltersChange,
|
handleFiltersChange,
|
||||||
fetchData,
|
fetchData,
|
||||||
|
isPolling,
|
||||||
|
startPolling,
|
||||||
|
stopPolling,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import {
|
|
||||||
DatabaseOutlined,
|
|
||||||
BarChartOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
ThunderboltOutlined,
|
|
||||||
PictureOutlined,
|
|
||||||
CalculatorOutlined,
|
|
||||||
SwapOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { FileImage, FileText, Music, Repeat, Video } from "lucide-react";
|
|
||||||
|
|
||||||
// 模板类型选项
|
|
||||||
export const templateTypes = [
|
|
||||||
{
|
|
||||||
value: "text",
|
|
||||||
label: "文本",
|
|
||||||
icon: FileText,
|
|
||||||
description: "处理文本数据的清洗模板",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "image",
|
|
||||||
label: "图片",
|
|
||||||
icon: FileImage,
|
|
||||||
description: "处理图像数据的清洗模板",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "video",
|
|
||||||
label: "视频",
|
|
||||||
icon: Video,
|
|
||||||
description: "处理视频数据的清洗模板",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "audio",
|
|
||||||
label: "音频",
|
|
||||||
icon: Music,
|
|
||||||
description: "处理音频数据的清洗模板",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "image-to-text",
|
|
||||||
label: "图片转文本",
|
|
||||||
icon: Repeat,
|
|
||||||
description: "图像识别转文本的处理模板",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 算子分类
|
|
||||||
export const OPERATOR_CATEGORIES = {
|
|
||||||
data: { name: "数据清洗", icon: <DatabaseOutlined />, color: "#1677ff" },
|
|
||||||
ml: { name: "机器学习", icon: <ThunderboltOutlined />, color: "#722ed1" },
|
|
||||||
vision: { name: "计算机视觉", icon: <PictureOutlined />, color: "#52c41a" },
|
|
||||||
nlp: { name: "自然语言处理", icon: <FileTextOutlined />, color: "#faad14" },
|
|
||||||
analysis: { name: "数据分析", icon: <BarChartOutlined />, color: "#f5222d" },
|
|
||||||
transform: { name: "数据转换", icon: <SwapOutlined />, color: "#13c2c2" },
|
|
||||||
io: { name: "输入输出", icon: <FileTextOutlined />, color: "#595959" },
|
|
||||||
math: { name: "数学计算", icon: <CalculatorOutlined />, color: "#fadb14" },
|
|
||||||
};
|
|
||||||
@@ -93,9 +93,13 @@ function cleaningTaskItem() {
|
|||||||
srcDatasetName: Mock.Random.ctitle(5, 15),
|
srcDatasetName: Mock.Random.ctitle(5, 15),
|
||||||
destDatasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
destDatasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
||||||
destDatasetName: Mock.Random.ctitle(5, 15),
|
destDatasetName: Mock.Random.ctitle(5, 15),
|
||||||
progress: Mock.Random.float(0, 100, 2, 2),
|
progress: {
|
||||||
|
finishedFileNum: Mock.Random.integer(0, 100),
|
||||||
|
process: Mock.Random.integer(0, 100),
|
||||||
|
totalFileNum: 100,
|
||||||
|
},
|
||||||
startedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
startedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||||
endedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
finishedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||||
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||||
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||||
instance: operatorList,
|
instance: operatorList,
|
||||||
@@ -244,7 +248,7 @@ module.exports = function (router) {
|
|||||||
const task = cleaningTaskList.find((j) => j.id === taskId);
|
const task = cleaningTaskList.find((j) => j.id === taskId);
|
||||||
|
|
||||||
if (task) {
|
if (task) {
|
||||||
task.status = "running";
|
task.status = "RUNNING";
|
||||||
task.startTime = new Date().toISOString();
|
task.startTime = new Date().toISOString();
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
@@ -252,7 +256,7 @@ module.exports = function (router) {
|
|||||||
msg: "Cleaning task execution started",
|
msg: "Cleaning task execution started",
|
||||||
data: {
|
data: {
|
||||||
executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
||||||
status: "running",
|
status: "RUNNING",
|
||||||
message: "Task execution started successfully",
|
message: "Task execution started successfully",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -271,7 +275,7 @@ module.exports = function (router) {
|
|||||||
const task = cleaningTaskList.find((j) => j.id === taskId);
|
const task = cleaningTaskList.find((j) => j.id === taskId);
|
||||||
|
|
||||||
if (task) {
|
if (task) {
|
||||||
task.status = "pending";
|
task.status = "PENDING";
|
||||||
task.endTime = new Date().toISOString();
|
task.endTime = new Date().toISOString();
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
|
|||||||
@@ -104,12 +104,14 @@ export default function TaskList() {
|
|||||||
key: "name",
|
key: "name",
|
||||||
fixed: "left",
|
fixed: "left",
|
||||||
width: 150,
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "源数据集",
|
title: "源数据集",
|
||||||
dataIndex: "srcDatasetId",
|
dataIndex: "srcDatasetId",
|
||||||
key: "srcDatasetId",
|
key: "srcDatasetId",
|
||||||
width: 150,
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
render: (_, record: CleansingTask) => {
|
render: (_, record: CleansingTask) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -128,6 +130,7 @@ export default function TaskList() {
|
|||||||
dataIndex: "destDatasetId",
|
dataIndex: "destDatasetId",
|
||||||
key: "destDatasetId",
|
key: "destDatasetId",
|
||||||
width: 150,
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
render: (_, record: CleansingTask) => {
|
render: (_, record: CleansingTask) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -147,47 +150,68 @@ export default function TaskList() {
|
|||||||
key: "status",
|
key: "status",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (status: any) => {
|
render: (status: any) => {
|
||||||
return <Badge color={status.color} text={status.label} />;
|
return <Badge color={status?.color} text={status?.label} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "开始时间",
|
|
||||||
dataIndex: "startedAt",
|
|
||||||
key: "startedAt",
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "结束时间",
|
|
||||||
dataIndex: "finishedAt",
|
|
||||||
key: "finishedAt",
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "进度",
|
title: "进度",
|
||||||
dataIndex: "progress",
|
dataIndex: "process",
|
||||||
key: "progress",
|
key: "process",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (progress: number) => (
|
render: (progress: number) => (
|
||||||
<Progress percent={progress} size="small" />
|
<Progress percent={progress} size="small" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "已处理文件数",
|
||||||
dataIndex: "createdAt",
|
dataIndex: "finishedFileNum",
|
||||||
key: "createdAt",
|
key: "finishedFileNum",
|
||||||
width: 180,
|
width: 150,
|
||||||
|
align: "right",
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "总文件数",
|
||||||
|
dataIndex: "totalFileNum",
|
||||||
|
key: "totalFileNum",
|
||||||
|
width: 150,
|
||||||
|
align: "right",
|
||||||
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "执行耗时",
|
title: "执行耗时",
|
||||||
dataIndex: "duration",
|
dataIndex: "duration",
|
||||||
key: "duration",
|
key: "duration",
|
||||||
width: 180,
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "开始时间",
|
||||||
|
dataIndex: "startedAt",
|
||||||
|
key: "startedAt",
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "结束时间",
|
||||||
|
dataIndex: "finishedAt",
|
||||||
|
key: "finishedAt",
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "创建时间",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
key: "createdAt",
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "数据量变化",
|
title: "数据量变化",
|
||||||
dataIndex: "dataSizeChange",
|
dataIndex: "dataSizeChange",
|
||||||
key: "dataSizeChange",
|
key: "dataSizeChange",
|
||||||
width: 180,
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
render: (_: any, record: CleansingTask) => {
|
render: (_: any, record: CleansingTask) => {
|
||||||
if (record.before !== undefined && record.after !== undefined) {
|
if (record.before !== undefined && record.after !== undefined) {
|
||||||
return `${record.before} → ${record.after}`;
|
return `${record.before} → ${record.after}`;
|
||||||
@@ -232,6 +256,7 @@ export default function TaskList() {
|
|||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
showViewToggle={true}
|
showViewToggle={true}
|
||||||
onReload={fetchData}
|
onReload={fetchData}
|
||||||
|
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||||
/>
|
/>
|
||||||
{/* Task List */}
|
{/* Task List */}
|
||||||
{viewMode === "card" ? (
|
{viewMode === "card" ? (
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const mapTask = (task: CleansingTask) => {
|
|||||||
const createdAt = formatDateTime(task.createdAt);
|
const createdAt = formatDateTime(task.createdAt);
|
||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
|
...task.progress,
|
||||||
createdAt,
|
createdAt,
|
||||||
startedAt,
|
startedAt,
|
||||||
finishedAt,
|
finishedAt,
|
||||||
@@ -105,18 +106,18 @@ export const mapTask = (task: CleansingTask) => {
|
|||||||
before,
|
before,
|
||||||
after,
|
after,
|
||||||
statistics: [
|
statistics: [
|
||||||
{ label: "进度", value: `${task.progress || 0}%` },
|
{ label: "进度", value: `${task?.progress?.process || 0}%` },
|
||||||
{
|
{
|
||||||
label: "执行耗时",
|
label: "执行耗时",
|
||||||
value: duration,
|
value: duration,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "处理前数据大小",
|
label: "已处理文件数",
|
||||||
value: task.beforeSize ? formatBytes(task.beforeSize) : "--",
|
value: task?.progress?.finishedFileNum || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "处理后数据大小",
|
label: "总文件数",
|
||||||
value: task.afterSize ? formatBytes(task.afterSize) : "--",
|
value: task?.progress?.totalFileNum || 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lastModified: formatDateTime(task.createdAt),
|
lastModified: formatDateTime(task.createdAt),
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export default function DatasetManagementPage() {
|
|||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
|
danger: true,
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class Request {
|
|||||||
try {
|
try {
|
||||||
const errorData = await processedResponse.json();
|
const errorData = await processedResponse.json();
|
||||||
error.data = errorData;
|
error.data = errorData;
|
||||||
message.error(`请求失败,错误信息: ${processedResponse.statusText}`);
|
// message.error(`请求失败,错误信息: ${processedResponse.statusText}`);
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略JSON解析错误
|
// 忽略JSON解析错误
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export function formatExecutionDuration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const formatDuration = (seconds: number): string => {
|
export const formatDuration = (seconds: number): string => {
|
||||||
|
if (seconds < 0) return "--";
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
return `${seconds} 秒`;
|
return `${seconds} 秒`;
|
||||||
} else if (seconds < 3600) {
|
} else if (seconds < 3600) {
|
||||||
|
|||||||
Reference in New Issue
Block a user