diff --git a/frontend/src/components/CardView.tsx b/frontend/src/components/CardView.tsx index 92363dd..57b739e 100644 --- a/frontend/src/components/CardView.tsx +++ b/frontend/src/components/CardView.tsx @@ -37,6 +37,7 @@ interface CardViewProps { | { key: string; label: string; + danger?: boolean; icon?: React.JSX.Element; onClick?: (item: T) => void; }[] @@ -169,82 +170,85 @@ function CardView(props: CardViewProps) { typeof operations === "function" ? operations(item) : operations; return (
-
+
{data.map((item) => (
- {/* Header */} -
-
- {item?.icon && ( -
- {item?.icon} -
- )} -
-
-

onView?.(item)} +
onView?.(item)} + style={{ cursor: onView ? "pointer" : "default" }} + > + {/* Header */} +
+
+ {item?.icon && ( +
- {item?.name} -

- {item?.status && ( - -
- {item?.status?.icon} - {item?.status?.label} -
-
- )} + {item?.icon} +
+ )} +
+
+

+ {item?.name} +

+ {item?.status && ( + +
+ {item?.status?.icon} + {item?.status?.label} +
+
+ )} +
+ {onFavorite && ( + onFavorite?.(item)} + /> + )}
- {onFavorite && ( - onFavorite?.(item)} - /> - )} -
-
- {/* Tags */} - +
+ {/* Tags */} + - {/* Description */} -

- - {item?.description} - -

+ {/* Description */} +

+ + {item?.description} + +

- {/* Statistics */} -
- {item?.statistics?.map((stat, idx) => ( -
-
- {stat?.label}: + {/* Statistics */} +
+ {item?.statistics?.map((stat, idx) => ( +
+
+ {stat?.label}: +
+
+ {stat?.value} +
-
- {stat?.value} -
-
- ))} + ))} +
diff --git a/frontend/src/hooks/useFetchData.ts b/frontend/src/hooks/useFetchData.ts index 441eeba..2754fe0 100644 --- a/frontend/src/hooks/useFetchData.ts +++ b/frontend/src/hooks/useFetchData.ts @@ -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( fetchFunc: (params?: any) => Promise, - 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(null); + // 表格数据 const [tableData, setTableData] = useState([]); // 设置加载状态 @@ -55,39 +73,108 @@ export default function useFetchData( return arr[0]; } - async function fetchData(extraParams = {}) { - const { keyword, filter, current, pageSize } = searchParams; - Loading.show(); - setLoading(true); - 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); - } catch (error) { - console.error(error) - message.error("数据获取失败,请稍后重试"); - } finally { - Loading.hide(); - setLoading(false); + // 清除轮询定时器 + 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, + ...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( () => { fetchData(); @@ -96,6 +183,16 @@ export default function useFetchData( searchParams?.keyword ? 500 : 0 ); + // 组件卸载时清理轮询 + useEffect(() => { + if (autoRefresh) { + startPolling(); + } + return () => { + clearPollingTimer(); + }; + }, [clearPollingTimer]); + return { loading, tableData, @@ -109,5 +206,8 @@ export default function useFetchData( setPagination, handleFiltersChange, fetchData, + isPolling, + startPolling, + stopPolling, }; } diff --git a/frontend/src/mock/cleansing.tsx b/frontend/src/mock/cleansing.tsx deleted file mode 100644 index 9d90184..0000000 --- a/frontend/src/mock/cleansing.tsx +++ /dev/null @@ -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: , color: "#1677ff" }, - ml: { name: "机器学习", icon: , color: "#722ed1" }, - vision: { name: "计算机视觉", icon: , color: "#52c41a" }, - nlp: { name: "自然语言处理", icon: , color: "#faad14" }, - analysis: { name: "数据分析", icon: , color: "#f5222d" }, - transform: { name: "数据转换", icon: , color: "#13c2c2" }, - io: { name: "输入输出", icon: , color: "#595959" }, - math: { name: "数学计算", icon: , color: "#fadb14" }, -}; diff --git a/frontend/src/mock/mock-seed/data-cleansing.cjs b/frontend/src/mock/mock-seed/data-cleansing.cjs index 6ecf051..c2b9a23 100644 --- a/frontend/src/mock/mock-seed/data-cleansing.cjs +++ b/frontend/src/mock/mock-seed/data-cleansing.cjs @@ -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({ diff --git a/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx b/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx index 873456b..fca97d2 100644 --- a/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx +++ b/frontend/src/pages/DataCleansing/Home/components/TaskList.tsx @@ -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 (