You've already forked DataMate
feat: fix the problem in the Operator Market frontend pages
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useDebouncedEffect(
|
||||
cb: () => void,
|
||||
deps: any[] = [],
|
||||
delay: number = 300
|
||||
) {
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
cb();
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [...(deps || []), delay]);
|
||||
}
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useDebouncedEffect(
|
||||
cb: () => void,
|
||||
deps: any[] = [],
|
||||
delay: number = 300
|
||||
) {
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
cb();
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [...(deps || []), delay]);
|
||||
}
|
||||
|
||||
@@ -1,236 +1,238 @@
|
||||
// 首页数据获取
|
||||
// 支持轮询功能,使用示例:
|
||||
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
|
||||
// fetchFunction,
|
||||
// mapFunction,
|
||||
// 5000, // 5秒轮询一次,默认30秒
|
||||
// true, // 是否自动开始轮询,默认 true
|
||||
// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组
|
||||
// );
|
||||
//
|
||||
// startPolling(); // 开始轮询
|
||||
// stopPolling(); // 停止轮询
|
||||
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
|
||||
// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数
|
||||
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: Partial<T>) => T = (data) => data as T,
|
||||
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||
autoRefresh: boolean = false, // 是否自动开始轮询,默认 false
|
||||
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
|
||||
pageOffset: number = 1
|
||||
) {
|
||||
const { message } = App.useApp();
|
||||
|
||||
// 轮询相关状态
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 表格数据
|
||||
const [tableData, setTableData] = useState<T[]>([]);
|
||||
// 设置加载状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 搜索参数
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
keyword: "",
|
||||
filter: {
|
||||
type: [] as string[],
|
||||
status: [] as string[],
|
||||
tags: [] as string[],
|
||||
},
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
});
|
||||
|
||||
// 分页配置
|
||||
const [pagination, setPagination] = useState({
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ["12", "24", "48"],
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
onChange: (current: number, pageSize?: number) => {
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
current,
|
||||
pageSize: pageSize || prev.pageSize,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
current: 1,
|
||||
filter: { ...searchParams.filter, ...searchFilters },
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeywordChange = (keyword: string) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
current: 1,
|
||||
keyword: keyword,
|
||||
});
|
||||
};
|
||||
|
||||
function getFirstOfArray(arr: string[]) {
|
||||
if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined;
|
||||
if (arr[0] === "all") return undefined;
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
// 清除轮询定时器
|
||||
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 promises = [
|
||||
fetchFunc({
|
||||
...Object.fromEntries(
|
||||
Object.entries(filter).filter(([_, value]) => value != null && value.length > 0)
|
||||
),
|
||||
...extraParams,
|
||||
keyword,
|
||||
type: getFirstOfArray(filter?.type) || undefined,
|
||||
status: getFirstOfArray(filter?.status) || undefined,
|
||||
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
||||
page: current - pageOffset,
|
||||
size: pageSize, // Use camelCase for HTTP query params
|
||||
}),
|
||||
...additionalPollingFuncs.map((func) => func()),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const { data } = results[0]; // 主要数据结果
|
||||
|
||||
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,
|
||||
additionalPollingFuncs,
|
||||
]
|
||||
);
|
||||
|
||||
// 开始轮询
|
||||
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();
|
||||
},
|
||||
[searchParams],
|
||||
searchParams?.keyword ? 500 : 0
|
||||
);
|
||||
|
||||
// 组件卸载时清理轮询
|
||||
useEffect(() => {
|
||||
if (autoRefresh) {
|
||||
startPolling();
|
||||
}
|
||||
return () => {
|
||||
clearPollingTimer();
|
||||
};
|
||||
}, [clearPollingTimer]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
tableData,
|
||||
pagination: {
|
||||
...pagination,
|
||||
current: searchParams.current,
|
||||
pageSize: searchParams.pageSize,
|
||||
},
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
setPagination,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
fetchData,
|
||||
isPolling,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
};
|
||||
}
|
||||
// 首页数据获取
|
||||
// 支持轮询功能,使用示例:
|
||||
// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData(
|
||||
// fetchFunction,
|
||||
// mapFunction,
|
||||
// 5000, // 5秒轮询一次,默认30秒
|
||||
// true, // 是否自动开始轮询,默认 true
|
||||
// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组
|
||||
// );
|
||||
//
|
||||
// startPolling(); // 开始轮询
|
||||
// stopPolling(); // 停止轮询
|
||||
// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时
|
||||
// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数
|
||||
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: Partial<T>) => T = (data) => data as T,
|
||||
pollingInterval: number = 30000, // 默认30秒轮询一次
|
||||
autoRefresh: boolean = false, // 是否自动开始轮询,默认 false
|
||||
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数
|
||||
pageOffset: number = 1
|
||||
) {
|
||||
const { message } = App.useApp();
|
||||
|
||||
// 轮询相关状态
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const pollingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 表格数据
|
||||
const [tableData, setTableData] = useState<T[]>([]);
|
||||
// 设置加载状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 搜索参数
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
keyword: "",
|
||||
filter: {
|
||||
type: [] as string[],
|
||||
status: [] as string[],
|
||||
tags: [] as string[],
|
||||
// 通用分类筛选(如算子市场的分类 ID 列表)
|
||||
categories: [] as string[],
|
||||
},
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
});
|
||||
|
||||
// 分页配置
|
||||
const [pagination, setPagination] = useState({
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ["12", "24", "48"],
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
onChange: (current: number, pageSize?: number) => {
|
||||
setSearchParams((prev) => ({
|
||||
...prev,
|
||||
current,
|
||||
pageSize: pageSize || prev.pageSize,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
current: 1,
|
||||
filter: { ...searchParams.filter, ...searchFilters },
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeywordChange = (keyword: string) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
current: 1,
|
||||
keyword: keyword,
|
||||
});
|
||||
};
|
||||
|
||||
function getFirstOfArray(arr: string[]) {
|
||||
if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined;
|
||||
if (arr[0] === "all") return undefined;
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
// 清除轮询定时器
|
||||
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 promises = [
|
||||
fetchFunc({
|
||||
...Object.fromEntries(
|
||||
Object.entries(filter).filter(([_, value]) => value != null && value.length > 0)
|
||||
),
|
||||
...extraParams,
|
||||
keyword,
|
||||
type: getFirstOfArray(filter?.type) || undefined,
|
||||
status: getFirstOfArray(filter?.status) || undefined,
|
||||
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
||||
page: current - pageOffset,
|
||||
size: pageSize, // Use camelCase for HTTP query params
|
||||
}),
|
||||
...additionalPollingFuncs.map((func) => func()),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const { data } = results[0]; // 主要数据结果
|
||||
|
||||
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,
|
||||
additionalPollingFuncs,
|
||||
]
|
||||
);
|
||||
|
||||
// 开始轮询
|
||||
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();
|
||||
},
|
||||
[searchParams],
|
||||
searchParams?.keyword ? 500 : 0
|
||||
);
|
||||
|
||||
// 组件卸载时清理轮询
|
||||
useEffect(() => {
|
||||
if (autoRefresh) {
|
||||
startPolling();
|
||||
}
|
||||
return () => {
|
||||
clearPollingTimer();
|
||||
};
|
||||
}, [clearPollingTimer]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
tableData,
|
||||
pagination: {
|
||||
...pagination,
|
||||
current: searchParams.current,
|
||||
pageSize: searchParams.pageSize,
|
||||
},
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
setPagination,
|
||||
handleFiltersChange,
|
||||
handleKeywordChange,
|
||||
fetchData,
|
||||
isPolling,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
// 自定义hook:页面离开前提示
|
||||
export function useLeavePrompt(shouldPrompt: boolean) {
|
||||
const navigate = useNavigate();
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const [nextPath, setNextPath] = useState<string | null>(null);
|
||||
|
||||
// 浏览器刷新/关闭
|
||||
useEffect(() => {
|
||||
const handler = (e: BeforeUnloadEvent) => {
|
||||
if (shouldPrompt) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
window.addEventListener("beforeunload", handler);
|
||||
return () => window.removeEventListener("beforeunload", handler);
|
||||
}, [shouldPrompt]);
|
||||
|
||||
// 路由切换拦截
|
||||
useEffect(() => {
|
||||
const unblock = (window as any).__REACT_ROUTER_DOM_HISTORY__?.block?.(
|
||||
(tx: any) => {
|
||||
if (shouldPrompt) {
|
||||
setShowPrompt(true);
|
||||
setNextPath(tx.location.pathname);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
if (unblock) unblock();
|
||||
};
|
||||
}, [shouldPrompt]);
|
||||
|
||||
const confirmLeave = useCallback(() => {
|
||||
setShowPrompt(false);
|
||||
if (nextPath) {
|
||||
navigate(nextPath, { replace: true });
|
||||
}
|
||||
}, [nextPath, navigate]);
|
||||
|
||||
return {
|
||||
showPrompt,
|
||||
setShowPrompt,
|
||||
confirmLeave,
|
||||
};
|
||||
}
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
// 自定义hook:页面离开前提示
|
||||
export function useLeavePrompt(shouldPrompt: boolean) {
|
||||
const navigate = useNavigate();
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const [nextPath, setNextPath] = useState<string | null>(null);
|
||||
|
||||
// 浏览器刷新/关闭
|
||||
useEffect(() => {
|
||||
const handler = (e: BeforeUnloadEvent) => {
|
||||
if (shouldPrompt) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
window.addEventListener("beforeunload", handler);
|
||||
return () => window.removeEventListener("beforeunload", handler);
|
||||
}, [shouldPrompt]);
|
||||
|
||||
// 路由切换拦截
|
||||
useEffect(() => {
|
||||
const unblock = (window as any).__REACT_ROUTER_DOM_HISTORY__?.block?.(
|
||||
(tx: any) => {
|
||||
if (shouldPrompt) {
|
||||
setShowPrompt(true);
|
||||
setNextPath(tx.location.pathname);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
if (unblock) unblock();
|
||||
};
|
||||
}, [shouldPrompt]);
|
||||
|
||||
const confirmLeave = useCallback(() => {
|
||||
setShowPrompt(false);
|
||||
if (nextPath) {
|
||||
navigate(nextPath, { replace: true });
|
||||
}
|
||||
}, [nextPath, navigate]);
|
||||
|
||||
return {
|
||||
showPrompt,
|
||||
setShowPrompt,
|
||||
confirmLeave,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
interface AnyObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function useSearchParams(): AnyObject {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const params: AnyObject = {};
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
params[key] = value;
|
||||
}
|
||||
return params;
|
||||
}, [search]);
|
||||
}
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
interface AnyObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function useSearchParams(): AnyObject {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const params: AnyObject = {};
|
||||
for (const [key, value] of urlParams.entries()) {
|
||||
params[key] = value;
|
||||
}
|
||||
return params;
|
||||
}, [search]);
|
||||
}
|
||||
|
||||
@@ -1,187 +1,187 @@
|
||||
import { TaskItem } from "@/pages/DataManagement/dataset.model";
|
||||
import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util";
|
||||
import { App } from "antd";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export function useFileSliceUpload(
|
||||
{
|
||||
preUpload,
|
||||
uploadChunk,
|
||||
cancelUpload,
|
||||
}: {
|
||||
preUpload: (id: string, params: any) => Promise<{ data: number }>;
|
||||
uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>;
|
||||
cancelUpload: ((reqId: number) => Promise<any>) | null;
|
||||
},
|
||||
showTaskCenter = true // 上传时是否显示任务中心
|
||||
) {
|
||||
const { message } = App.useApp();
|
||||
const [taskList, setTaskList] = useState<TaskItem[]>([]);
|
||||
const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序
|
||||
|
||||
const createTask = (detail: any = {}) => {
|
||||
const { dataset } = detail;
|
||||
const title = `上传数据集: ${dataset.name} `;
|
||||
const controller = new AbortController();
|
||||
const task: TaskItem = {
|
||||
key: dataset.id,
|
||||
title,
|
||||
percent: 0,
|
||||
reqId: -1,
|
||||
controller,
|
||||
size: 0,
|
||||
updateEvent: detail.updateEvent,
|
||||
hasArchive: detail.hasArchive,
|
||||
};
|
||||
taskListRef.current = [task, ...taskListRef.current];
|
||||
|
||||
setTaskList(taskListRef.current);
|
||||
return task;
|
||||
};
|
||||
|
||||
const updateTaskList = (task: TaskItem) => {
|
||||
taskListRef.current = taskListRef.current.map((item) =>
|
||||
item.key === task.key ? task : item
|
||||
);
|
||||
setTaskList(taskListRef.current);
|
||||
};
|
||||
|
||||
const removeTask = (task: TaskItem) => {
|
||||
const { key } = task;
|
||||
taskListRef.current = taskListRef.current.filter(
|
||||
(item) => item.key !== key
|
||||
);
|
||||
setTaskList(taskListRef.current);
|
||||
if (task.isCancel && task.cancelFn) {
|
||||
task.cancelFn();
|
||||
}
|
||||
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
|
||||
if (showTaskCenter) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show:task-popover", { detail: { show: false } })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async function buildFormData({ file, reqId, i, j }) {
|
||||
const formData = new FormData();
|
||||
const { slices, name, size } = file;
|
||||
const checkSum = await calculateSHA256(slices[j]);
|
||||
formData.append("file", slices[j]);
|
||||
formData.append("reqId", reqId.toString());
|
||||
formData.append("fileNo", (i + 1).toString());
|
||||
formData.append("chunkNo", (j + 1).toString());
|
||||
formData.append("fileName", name);
|
||||
formData.append("fileSize", size.toString());
|
||||
formData.append("totalChunkNum", slices.length.toString());
|
||||
formData.append("checkSumHex", checkSum);
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function uploadSlice(task: TaskItem, fileInfo) {
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
const { reqId, key } = task;
|
||||
const { loaded, i, j, files, totalSize } = fileInfo;
|
||||
const formData = await buildFormData({
|
||||
file: files[i],
|
||||
i,
|
||||
j,
|
||||
reqId,
|
||||
});
|
||||
|
||||
let newTask = { ...task };
|
||||
await uploadChunk(key, formData, {
|
||||
onUploadProgress: (e) => {
|
||||
const loadedSize = loaded + e.loaded;
|
||||
const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2);
|
||||
|
||||
newTask = {
|
||||
...newTask,
|
||||
...taskListRef.current.find((item) => item.key === key),
|
||||
size: loadedSize,
|
||||
percent: curPercent >= 100 ? 99.99 : curPercent,
|
||||
};
|
||||
updateTaskList(newTask);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadFile({ task, files, totalSize }) {
|
||||
const { data: reqId } = await preUpload(task.key, {
|
||||
totalFileNum: files.length,
|
||||
totalSize,
|
||||
datasetId: task.key,
|
||||
hasArchive: task.hasArchive,
|
||||
});
|
||||
|
||||
const newTask: TaskItem = {
|
||||
...task,
|
||||
reqId,
|
||||
isCancel: false,
|
||||
cancelFn: () => {
|
||||
task.controller.abort();
|
||||
cancelUpload?.(reqId);
|
||||
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
|
||||
},
|
||||
};
|
||||
updateTaskList(newTask);
|
||||
if (showTaskCenter) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show:task-popover", { detail: { show: true } })
|
||||
);
|
||||
}
|
||||
// // 更新数据状态
|
||||
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
|
||||
|
||||
let loaded = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { slices } = files[i];
|
||||
for (let j = 0; j < slices.length; j++) {
|
||||
await uploadSlice(newTask, {
|
||||
loaded,
|
||||
i,
|
||||
j,
|
||||
files,
|
||||
totalSize,
|
||||
});
|
||||
loaded += slices[j].size;
|
||||
}
|
||||
}
|
||||
removeTask(newTask);
|
||||
}
|
||||
|
||||
const handleUpload = async ({ task, files }) => {
|
||||
const isErrorFile = await checkIsFilesExist(files);
|
||||
if (isErrorFile) {
|
||||
message.error("文件被修改或删除,请重新选择文件上传");
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: false,
|
||||
...taskListRef.current.find((item) => item.key === task.key),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
||||
await uploadFile({ task, files, totalSize });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
message.error("文件上传失败,请稍后重试");
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: true,
|
||||
...taskListRef.current.find((item) => item.key === task.key),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
taskList,
|
||||
createTask,
|
||||
removeTask,
|
||||
handleUpload,
|
||||
};
|
||||
}
|
||||
import { TaskItem } from "@/pages/DataManagement/dataset.model";
|
||||
import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util";
|
||||
import { App } from "antd";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export function useFileSliceUpload(
|
||||
{
|
||||
preUpload,
|
||||
uploadChunk,
|
||||
cancelUpload,
|
||||
}: {
|
||||
preUpload: (id: string, params: any) => Promise<{ data: number }>;
|
||||
uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>;
|
||||
cancelUpload: ((reqId: number) => Promise<any>) | null;
|
||||
},
|
||||
showTaskCenter = true // 上传时是否显示任务中心
|
||||
) {
|
||||
const { message } = App.useApp();
|
||||
const [taskList, setTaskList] = useState<TaskItem[]>([]);
|
||||
const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序
|
||||
|
||||
const createTask = (detail: any = {}) => {
|
||||
const { dataset } = detail;
|
||||
const title = `上传数据集: ${dataset.name} `;
|
||||
const controller = new AbortController();
|
||||
const task: TaskItem = {
|
||||
key: dataset.id,
|
||||
title,
|
||||
percent: 0,
|
||||
reqId: -1,
|
||||
controller,
|
||||
size: 0,
|
||||
updateEvent: detail.updateEvent,
|
||||
hasArchive: detail.hasArchive,
|
||||
};
|
||||
taskListRef.current = [task, ...taskListRef.current];
|
||||
|
||||
setTaskList(taskListRef.current);
|
||||
return task;
|
||||
};
|
||||
|
||||
const updateTaskList = (task: TaskItem) => {
|
||||
taskListRef.current = taskListRef.current.map((item) =>
|
||||
item.key === task.key ? task : item
|
||||
);
|
||||
setTaskList(taskListRef.current);
|
||||
};
|
||||
|
||||
const removeTask = (task: TaskItem) => {
|
||||
const { key } = task;
|
||||
taskListRef.current = taskListRef.current.filter(
|
||||
(item) => item.key !== key
|
||||
);
|
||||
setTaskList(taskListRef.current);
|
||||
if (task.isCancel && task.cancelFn) {
|
||||
task.cancelFn();
|
||||
}
|
||||
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
|
||||
if (showTaskCenter) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show:task-popover", { detail: { show: false } })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async function buildFormData({ file, reqId, i, j }) {
|
||||
const formData = new FormData();
|
||||
const { slices, name, size } = file;
|
||||
const checkSum = await calculateSHA256(slices[j]);
|
||||
formData.append("file", slices[j]);
|
||||
formData.append("reqId", reqId.toString());
|
||||
formData.append("fileNo", (i + 1).toString());
|
||||
formData.append("chunkNo", (j + 1).toString());
|
||||
formData.append("fileName", name);
|
||||
formData.append("fileSize", size.toString());
|
||||
formData.append("totalChunkNum", slices.length.toString());
|
||||
formData.append("checkSumHex", checkSum);
|
||||
return formData;
|
||||
}
|
||||
|
||||
async function uploadSlice(task: TaskItem, fileInfo) {
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
const { reqId, key } = task;
|
||||
const { loaded, i, j, files, totalSize } = fileInfo;
|
||||
const formData = await buildFormData({
|
||||
file: files[i],
|
||||
i,
|
||||
j,
|
||||
reqId,
|
||||
});
|
||||
|
||||
let newTask = { ...task };
|
||||
await uploadChunk(key, formData, {
|
||||
onUploadProgress: (e) => {
|
||||
const loadedSize = loaded + e.loaded;
|
||||
const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2);
|
||||
|
||||
newTask = {
|
||||
...newTask,
|
||||
...taskListRef.current.find((item) => item.key === key),
|
||||
size: loadedSize,
|
||||
percent: curPercent >= 100 ? 99.99 : curPercent,
|
||||
};
|
||||
updateTaskList(newTask);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadFile({ task, files, totalSize }) {
|
||||
const { data: reqId } = await preUpload(task.key, {
|
||||
totalFileNum: files.length,
|
||||
totalSize,
|
||||
datasetId: task.key,
|
||||
hasArchive: task.hasArchive,
|
||||
});
|
||||
|
||||
const newTask: TaskItem = {
|
||||
...task,
|
||||
reqId,
|
||||
isCancel: false,
|
||||
cancelFn: () => {
|
||||
task.controller.abort();
|
||||
cancelUpload?.(reqId);
|
||||
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
|
||||
},
|
||||
};
|
||||
updateTaskList(newTask);
|
||||
if (showTaskCenter) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show:task-popover", { detail: { show: true } })
|
||||
);
|
||||
}
|
||||
// // 更新数据状态
|
||||
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
|
||||
|
||||
let loaded = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { slices } = files[i];
|
||||
for (let j = 0; j < slices.length; j++) {
|
||||
await uploadSlice(newTask, {
|
||||
loaded,
|
||||
i,
|
||||
j,
|
||||
files,
|
||||
totalSize,
|
||||
});
|
||||
loaded += slices[j].size;
|
||||
}
|
||||
}
|
||||
removeTask(newTask);
|
||||
}
|
||||
|
||||
const handleUpload = async ({ task, files }) => {
|
||||
const isErrorFile = await checkIsFilesExist(files);
|
||||
if (isErrorFile) {
|
||||
message.error("文件被修改或删除,请重新选择文件上传");
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: false,
|
||||
...taskListRef.current.find((item) => item.key === task.key),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
||||
await uploadFile({ task, files, totalSize });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
message.error("文件上传失败,请稍后重试");
|
||||
removeTask({
|
||||
...task,
|
||||
isCancel: true,
|
||||
...taskListRef.current.find((item) => item.key === task.key),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
taskList,
|
||||
createTask,
|
||||
removeTask,
|
||||
handleUpload,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { createStyles } from "antd-style";
|
||||
|
||||
const useStyle = createStyles(({ css, token }) => {
|
||||
const { antCls } = token;
|
||||
return {
|
||||
customTable: css`
|
||||
${antCls}-table {
|
||||
${antCls}-table-container {
|
||||
${antCls}-table-body, ${antCls}-table-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${token.colorBorder} transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyle;
|
||||
import { createStyles } from "antd-style";
|
||||
|
||||
const useStyle = createStyles(({ css, token }) => {
|
||||
const { antCls } = token;
|
||||
return {
|
||||
customTable: css`
|
||||
${antCls}-table {
|
||||
${antCls}-table-container {
|
||||
${antCls}-table-body, ${antCls}-table-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${token.colorBorder} transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyle;
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { message } from "antd";
|
||||
import { getTagConfigUsingGet } from "../pages/DataAnnotation/annotation.api";
|
||||
import type { LabelStudioTagConfig } from "../pages/DataAnnotation/annotation.tagconfig";
|
||||
import { parseTagConfig, type TagOption } from "../pages/DataAnnotation/annotation.tagconfig";
|
||||
|
||||
interface UseTagConfigReturn {
|
||||
config: LabelStudioTagConfig | null;
|
||||
objectOptions: TagOption[];
|
||||
controlOptions: TagOption[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and manage Label Studio tag configuration
|
||||
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
|
||||
*/
|
||||
export function useTagConfig(includeLabelingOnly: boolean = true): UseTagConfigReturn {
|
||||
const [config, setConfig] = useState<LabelStudioTagConfig | null>(null);
|
||||
const [objectOptions, setObjectOptions] = useState<TagOption[]>([]);
|
||||
const [controlOptions, setControlOptions] = useState<TagOption[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getTagConfigUsingGet();
|
||||
if (response.code === 200 && response.data) {
|
||||
const tagConfig: LabelStudioTagConfig = response.data;
|
||||
setConfig(tagConfig);
|
||||
|
||||
const { objectOptions: objects, controlOptions: controls } =
|
||||
parseTagConfig(tagConfig, includeLabelingOnly);
|
||||
setObjectOptions(objects);
|
||||
setControlOptions(controls);
|
||||
} else {
|
||||
const errorMsg = response.message || "获取标签配置失败";
|
||||
setError(errorMsg);
|
||||
message.error(errorMsg);
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.message || "加载标签配置时出错";
|
||||
setError(errorMsg);
|
||||
console.error("Failed to fetch tag config:", err);
|
||||
message.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
config,
|
||||
objectOptions,
|
||||
controlOptions,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchConfig,
|
||||
};
|
||||
}
|
||||
import { useState, useEffect } from "react";
|
||||
import { message } from "antd";
|
||||
import { getTagConfigUsingGet } from "../pages/DataAnnotation/annotation.api";
|
||||
import type { LabelStudioTagConfig } from "../pages/DataAnnotation/annotation.tagconfig";
|
||||
import { parseTagConfig, type TagOption } from "../pages/DataAnnotation/annotation.tagconfig";
|
||||
|
||||
interface UseTagConfigReturn {
|
||||
config: LabelStudioTagConfig | null;
|
||||
objectOptions: TagOption[];
|
||||
controlOptions: TagOption[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and manage Label Studio tag configuration
|
||||
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
|
||||
*/
|
||||
export function useTagConfig(includeLabelingOnly: boolean = true): UseTagConfigReturn {
|
||||
const [config, setConfig] = useState<LabelStudioTagConfig | null>(null);
|
||||
const [objectOptions, setObjectOptions] = useState<TagOption[]>([]);
|
||||
const [controlOptions, setControlOptions] = useState<TagOption[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getTagConfigUsingGet();
|
||||
if (response.code === 200 && response.data) {
|
||||
const tagConfig: LabelStudioTagConfig = response.data;
|
||||
setConfig(tagConfig);
|
||||
|
||||
const { objectOptions: objects, controlOptions: controls } =
|
||||
parseTagConfig(tagConfig, includeLabelingOnly);
|
||||
setObjectOptions(objects);
|
||||
setControlOptions(controls);
|
||||
} else {
|
||||
const errorMsg = response.message || "获取标签配置失败";
|
||||
setError(errorMsg);
|
||||
message.error(errorMsg);
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.message || "加载标签配置时出错";
|
||||
setError(errorMsg);
|
||||
console.error("Failed to fetch tag config:", err);
|
||||
message.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
config,
|
||||
objectOptions,
|
||||
controlOptions,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchConfig,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user