feat(data-annotation): 添加任务预加载功能以提升用户体验

- 引入 UpsertAnnotationResponse 类型定义用于处理标注更新响应
- 移除废弃的 mergeTaskPages 函数并优化任务列表合并逻辑
- 新增 prefetchSeqRef 和 prefetching 状态管理预加载过程
- 实现 startPrefetchTasks 函数用于后台预加载剩余页的任务数据
- 更新 loadTasks 函数移除 refresh 模式并集成预加载机制
- 修改标注保存逻辑直接更新本地任务状态而非重新加载全部数据
- 在加载按钮中显示预加载状态提示用户当前操作进度
- 项目切换时重置预加载序列号确保状态一致性
This commit is contained in:
2026-01-27 19:45:25 +08:00
parent 1158647217
commit a28b427e21

View File

@@ -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<EditorTaskListResponse> | null | undefined,
fallbackPage: number,
@@ -193,6 +195,7 @@ export default function LabelStudioTextEditor() {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const initSeqRef = useRef(0);
const expectedTaskIdRef = useRef<number | null>(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<string>("");
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<EditorTaskListResponse>;
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<EditorTaskListResponse>;
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<EditorTaskListResponse>;
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<EditorTaskListResponse>;
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<UpsertAnnotationResponse>;
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() {
<Button
size="small"
loading={loadingMore}
disabled={loadingTasks}
disabled={loadingTasks || prefetching}
onClick={() => loadTasks({ mode: "append" })}
>
</Button>
{prefetching && (
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
...
</Typography.Text>
)}
{taskTotal > 0 && (
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
{tasks.length}/{taskTotal}