From a28b427e21b0fa2f0a8ac598c40ab105789b016c Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Tue, 27 Jan 2026 19:45:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(data-annotation):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E9=A2=84=E5=8A=A0=E8=BD=BD=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 UpsertAnnotationResponse 类型定义用于处理标注更新响应 - 移除废弃的 mergeTaskPages 函数并优化任务列表合并逻辑 - 新增 prefetchSeqRef 和 prefetching 状态管理预加载过程 - 实现 startPrefetchTasks 函数用于后台预加载剩余页的任务数据 - 更新 loadTasks 函数移除 refresh 模式并集成预加载机制 - 修改标注保存逻辑直接更新本地任务状态而非重新加载全部数据 - 在加载按钮中显示预加载状态提示用户当前操作进度 - 项目切换时重置预加载序列号确保状态一致性 --- .../Annotate/LabelStudioTextEditor.tsx | 120 +++++++++++------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index f1bb4b6..6ace32c 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -69,6 +69,11 @@ type EditorTaskListResponse = { size?: number; }; +type UpsertAnnotationResponse = { + annotationId?: string; + updatedAt?: string; +}; + type ExportPayload = { taskId?: number | string | null; fileId?: string | null; @@ -161,9 +166,6 @@ const mergeTaskItems = (base: EditorTaskListItem[], next: EditorTaskListItem[]) return merged; }; -const mergeTaskPages = (pages: EditorTaskListItem[][]) => - pages.reduce((acc, page) => mergeTaskItems(acc, page), []); - const normalizeTaskListResponse = ( response: ApiResponse | null | undefined, fallbackPage: number, @@ -193,6 +195,7 @@ export default function LabelStudioTextEditor() { const iframeRef = useRef(null); const initSeqRef = useRef(0); const expectedTaskIdRef = useRef(null); + const prefetchSeqRef = useRef(0); const exportCheckRef = useRef<{ requestId: string; resolve: (payload?: ExportPayload) => void; @@ -216,6 +219,7 @@ export default function LabelStudioTextEditor() { const [taskTotal, setTaskTotal] = useState(0); const [taskTotalPages, setTaskTotalPages] = useState(0); const [loadingMore, setLoadingMore] = useState(false); + const [prefetching, setPrefetching] = useState(false); const [selectedFileId, setSelectedFileId] = useState(""); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [autoSaveOnSwitch, setAutoSaveOnSwitch] = useState(false); @@ -267,8 +271,41 @@ export default function LabelStudioTextEditor() { }); }, []); + const startPrefetchTasks = useCallback((startPage: number, totalPages: number) => { + if (!projectId) return; + if (startPage >= totalPages) { + setPrefetching(false); + return; + } + const seq = ++prefetchSeqRef.current; + setPrefetching(true); + const run = async () => { + for (let page = startPage; page < totalPages; page += 1) { + if (prefetchSeqRef.current !== seq) return; + try { + const resp = (await listEditorTasksUsingGet(projectId, { + page, + size: TASK_PAGE_SIZE, + })) as ApiResponse; + const normalized = normalizeTaskListResponse(resp, page); + setTasks((prev) => mergeTaskItems(prev, normalized.items)); + setTaskPage((prev) => Math.max(prev, normalized.page)); + setTaskTotal(normalized.total); + setTaskTotalPages(normalized.totalPages); + } catch (e) { + console.error(e); + break; + } + } + if (prefetchSeqRef.current === seq) { + setPrefetching(false); + } + }; + void run(); + }, [projectId]); + const loadTasks = useCallback(async (options?: { - mode?: "reset" | "append" | "refresh"; + mode?: "reset" | "append"; silent?: boolean; }) => { if (!projectId) return; @@ -277,53 +314,33 @@ export default function LabelStudioTextEditor() { if (mode === "append" && taskTotalPages > 0 && taskPage + 1 >= taskTotalPages) { return; } + if (mode === "reset") { + prefetchSeqRef.current += 1; + setPrefetching(false); + } if (mode === "append") { setLoadingMore(true); } else if (!silent) { setLoadingTasks(true); } try { - if (mode === "refresh") { - const firstResp = (await listEditorTasksUsingGet(projectId, { - page: TASK_PAGE_START, - size: TASK_PAGE_SIZE, - })) as ApiResponse; - const firstNormalized = normalizeTaskListResponse(firstResp, TASK_PAGE_START); - const lastPage = Math.min( - taskPage, - Math.max(firstNormalized.totalPages - 1, TASK_PAGE_START), - ); - const pages: EditorTaskListItem[][] = [firstNormalized.items]; - for (let page = TASK_PAGE_START + 1; page <= lastPage; page += 1) { - const resp = (await listEditorTasksUsingGet(projectId, { - page, - size: TASK_PAGE_SIZE, - })) as ApiResponse; - const normalized = normalizeTaskListResponse(resp, page); - pages.push(normalized.items); - } - const mergedItems = mergeTaskPages(pages); - setTasks(mergedItems); - setTaskPage(lastPage); - setTaskTotal(firstNormalized.total); - setTaskTotalPages(firstNormalized.totalPages); - updateTaskSelection(mergedItems); - return; - } - const nextPage = mode === "append" ? taskPage + 1 : TASK_PAGE_START; const resp = (await listEditorTasksUsingGet(projectId, { page: nextPage, size: TASK_PAGE_SIZE, })) as ApiResponse; const normalized = normalizeTaskListResponse(resp, nextPage); - const nextItems = - mode === "append" ? mergeTaskItems(tasks, normalized.items) : normalized.items; - setTasks(nextItems); - setTaskPage(normalized.page); + if (mode === "append") { + setTasks((prev) => mergeTaskItems(prev, normalized.items)); + setTaskPage((prev) => Math.max(prev, normalized.page)); + } else { + setTasks(normalized.items); + setTaskPage(normalized.page); + updateTaskSelection(normalized.items); + } setTaskTotal(normalized.total); setTaskTotalPages(normalized.totalPages); - updateTaskSelection(nextItems); + startPrefetchTasks(normalized.page + 1, normalized.totalPages); } catch (e) { console.error(e); if (!silent && mode !== "append") message.error("获取文件列表失败"); @@ -340,7 +357,7 @@ export default function LabelStudioTextEditor() { setLoadingTasks(false); } } - }, [message, projectId, taskPage, taskTotalPages, tasks, updateTaskSelection]); + }, [message, projectId, startPrefetchTasks, taskPage, taskTotalPages, updateTaskSelection]); const initEditorForFile = useCallback(async (fileId: string, segmentIdx?: number) => { if (!project?.supported) return; @@ -503,12 +520,23 @@ export default function LabelStudioTextEditor() { setSaving(true); try { - await upsertEditorAnnotationUsingPut(projectId, String(fileId), { + const resp = (await upsertEditorAnnotationUsingPut(projectId, String(fileId), { annotation, segmentIndex, - }); + })) as ApiResponse; + const updatedAt = resp?.data?.updatedAt; message.success("标注已保存"); - await loadTasks({ mode: "refresh", silent: true }); + setTasks((prev) => + prev.map((item) => + item.fileId === String(fileId) + ? { + ...item, + hasAnnotation: true, + annotationUpdatedAt: updatedAt || item.annotationUpdatedAt, + } + : item + ) + ); const snapshotKey = buildSnapshotKey(String(fileId), segmentIndex); const snapshot = buildAnnotationSnapshot(isRecord(annotation) ? annotation : undefined); @@ -538,7 +566,6 @@ export default function LabelStudioTextEditor() { }, [ advanceAfterSave, currentSegmentIndex, - loadTasks, message, projectId, segmented, @@ -695,6 +722,8 @@ export default function LabelStudioTextEditor() { setTaskTotal(0); setTaskTotalPages(0); setLoadingMore(false); + prefetchSeqRef.current += 1; + setPrefetching(false); setSelectedFileId(""); initSeqRef.current = 0; setLsReady(false); @@ -852,11 +881,16 @@ export default function LabelStudioTextEditor() { + {prefetching && ( + + 后台加载中... + + )} {taskTotal > 0 && ( 已加载 {tasks.length}/{taskTotal}