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}