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;
|
||||
label: string;
|
||||
danger?: boolean;
|
||||
icon?: React.JSX.Element;
|
||||
onClick?: (item: T) => void;
|
||||
}[]
|
||||
@@ -169,13 +170,18 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
||||
typeof operations === "function" ? operations(item) : operations;
|
||||
return (
|
||||
<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) => (
|
||||
<div
|
||||
key={item.id}
|
||||
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"
|
||||
onClick={() => onView?.(item)}
|
||||
style={{ cursor: onView ? "pointer" : "default" }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
@@ -192,10 +198,7 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<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)}
|
||||
className={`text-base flex-1 text-ellipsis overflow-hidden whitespace-nowrap font-semibold text-gray-900 truncate`}
|
||||
>
|
||||
{item?.name}
|
||||
</h3>
|
||||
@@ -247,6 +250,7 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-t-gray-200">
|
||||
|
||||
@@ -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 Loading from "@/utils/loading";
|
||||
import { App } from "antd";
|
||||
|
||||
export default function useFetchData<T>(
|
||||
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 [isPolling, setIsPolling] = useState(false);
|
||||
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 表格数据
|
||||
const [tableData, setTableData] = useState<T[]>([]);
|
||||
// 设置加载状态
|
||||
@@ -55,10 +73,29 @@ export default function useFetchData<T>(
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
async function fetchData(extraParams = {}) {
|
||||
// 清除轮询定时器
|
||||
const clearPollingTimer = useCallback(() => {
|
||||
if (pollingTimerRef.current) {
|
||||
clearTimeout(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
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,
|
||||
@@ -79,15 +116,65 @@ export default function useFetchData<T>(
|
||||
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)
|
||||
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(
|
||||
() => {
|
||||
fetchData();
|
||||
@@ -96,6 +183,16 @@ export default function useFetchData<T>(
|
||||
searchParams?.keyword ? 500 : 0
|
||||
);
|
||||
|
||||
// 组件卸载时清理轮询
|
||||
useEffect(() => {
|
||||
if (autoRefresh) {
|
||||
startPolling();
|
||||
}
|
||||
return () => {
|
||||
clearPollingTimer();
|
||||
};
|
||||
}, [clearPollingTimer]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
tableData,
|
||||
@@ -109,5 +206,8 @@ export default function useFetchData<T>(
|
||||
setPagination,
|
||||
handleFiltersChange,
|
||||
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),
|
||||
destDatasetId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
||||
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"),
|
||||
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"),
|
||||
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
|
||||
instance: operatorList,
|
||||
@@ -244,7 +248,7 @@ module.exports = function (router) {
|
||||
const task = cleaningTaskList.find((j) => j.id === taskId);
|
||||
|
||||
if (task) {
|
||||
task.status = "running";
|
||||
task.status = "RUNNING";
|
||||
task.startTime = new Date().toISOString();
|
||||
|
||||
res.send({
|
||||
@@ -252,7 +256,7 @@ module.exports = function (router) {
|
||||
msg: "Cleaning task execution started",
|
||||
data: {
|
||||
executionId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
|
||||
status: "running",
|
||||
status: "RUNNING",
|
||||
message: "Task execution started successfully",
|
||||
},
|
||||
});
|
||||
@@ -271,7 +275,7 @@ module.exports = function (router) {
|
||||
const task = cleaningTaskList.find((j) => j.id === taskId);
|
||||
|
||||
if (task) {
|
||||
task.status = "pending";
|
||||
task.status = "PENDING";
|
||||
task.endTime = new Date().toISOString();
|
||||
|
||||
res.send({
|
||||
|
||||
@@ -104,12 +104,14 @@ export default function TaskList() {
|
||||
key: "name",
|
||||
fixed: "left",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "源数据集",
|
||||
dataIndex: "srcDatasetId",
|
||||
key: "srcDatasetId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, record: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -128,6 +130,7 @@ export default function TaskList() {
|
||||
dataIndex: "destDatasetId",
|
||||
key: "destDatasetId",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, record: CleansingTask) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -147,47 +150,68 @@ export default function TaskList() {
|
||||
key: "status",
|
||||
width: 100,
|
||||
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: "进度",
|
||||
dataIndex: "progress",
|
||||
key: "progress",
|
||||
dataIndex: "process",
|
||||
key: "process",
|
||||
width: 200,
|
||||
render: (progress: number) => (
|
||||
<Progress percent={progress} size="small" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
width: 180,
|
||||
title: "已处理文件数",
|
||||
dataIndex: "finishedFileNum",
|
||||
key: "finishedFileNum",
|
||||
width: 150,
|
||||
align: "right",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "总文件数",
|
||||
dataIndex: "totalFileNum",
|
||||
key: "totalFileNum",
|
||||
width: 150,
|
||||
align: "right",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "执行耗时",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
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: "数据量变化",
|
||||
dataIndex: "dataSizeChange",
|
||||
key: "dataSizeChange",
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: CleansingTask) => {
|
||||
if (record.before !== undefined && record.after !== undefined) {
|
||||
return `${record.before} → ${record.after}`;
|
||||
@@ -232,6 +256,7 @@ export default function TaskList() {
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
||||
/>
|
||||
{/* Task List */}
|
||||
{viewMode === "card" ? (
|
||||
|
||||
@@ -95,6 +95,7 @@ export const mapTask = (task: CleansingTask) => {
|
||||
const createdAt = formatDateTime(task.createdAt);
|
||||
return {
|
||||
...task,
|
||||
...task.progress,
|
||||
createdAt,
|
||||
startedAt,
|
||||
finishedAt,
|
||||
@@ -105,18 +106,18 @@ export const mapTask = (task: CleansingTask) => {
|
||||
before,
|
||||
after,
|
||||
statistics: [
|
||||
{ label: "进度", value: `${task.progress || 0}%` },
|
||||
{ label: "进度", value: `${task?.progress?.process || 0}%` },
|
||||
{
|
||||
label: "执行耗时",
|
||||
value: duration,
|
||||
},
|
||||
{
|
||||
label: "处理前数据大小",
|
||||
value: task.beforeSize ? formatBytes(task.beforeSize) : "--",
|
||||
label: "已处理文件数",
|
||||
value: task?.progress?.finishedFileNum || 0,
|
||||
},
|
||||
{
|
||||
label: "处理后数据大小",
|
||||
value: task.afterSize ? formatBytes(task.afterSize) : "--",
|
||||
label: "总文件数",
|
||||
value: task?.progress?.totalFileNum || 0,
|
||||
},
|
||||
],
|
||||
lastModified: formatDateTime(task.createdAt),
|
||||
|
||||
@@ -158,6 +158,7 @@ export default function DatasetManagementPage() {
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: (item: Dataset) => handleDeleteDataset(item.id),
|
||||
},
|
||||
|
||||
@@ -202,7 +202,7 @@ class Request {
|
||||
try {
|
||||
const errorData = await processedResponse.json();
|
||||
error.data = errorData;
|
||||
message.error(`请求失败,错误信息: ${processedResponse.statusText}`);
|
||||
// message.error(`请求失败,错误信息: ${processedResponse.statusText}`);
|
||||
} catch {
|
||||
// 忽略JSON解析错误
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export function formatExecutionDuration(
|
||||
}
|
||||
|
||||
export const formatDuration = (seconds: number): string => {
|
||||
if (seconds < 0) return "--";
|
||||
if (seconds < 60) {
|
||||
return `${seconds} 秒`;
|
||||
} else if (seconds < 3600) {
|
||||
|
||||
Reference in New Issue
Block a user