feat: fix the problem in the Operator Market frontend pages

This commit is contained in:
root
2025-12-29 11:38:47 +08:00
parent 29e4a333a9
commit 844add27ea
213 changed files with 45547 additions and 45537 deletions

View File

@@ -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]);
}

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -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]);
}

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -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,
};
}