feat(annotation): 实现任务列表分页加载和优化排序功能

- 添加分页相关字段到EditorTaskListResponse类型定义
- 定义TASK_PAGE_START和TASK_PAGE_SIZE常量及NormalizedTaskList类型
- 实现mergeTaskItems、mergeTaskPages和normalizeTaskListResponse工具函数
- 添加taskPage、taskTotal、taskTotalPages和loadingMore状态管理
- 优化后端查询逻辑,使用case语句实现标注状态排序
- 集成外连接查询同时获取文件信息和标注结果
- 改进前端任务列表的数据合并和分页加载机制
This commit is contained in:
2026-01-27 18:22:17 +08:00
parent 3a93098b57
commit 1158647217
2 changed files with 178 additions and 43 deletions

View File

@@ -63,6 +63,10 @@ type EditorTaskResponse = {
type EditorTaskListResponse = {
content?: EditorTaskListItem[];
totalElements?: number;
totalPages?: number;
page?: number;
size?: number;
};
type ExportPayload = {
@@ -76,6 +80,16 @@ type ExportPayload = {
type SwitchDecision = "save" | "discard" | "cancel";
const LSF_IFRAME_SRC = "/lsf/lsf.html";
const TASK_PAGE_START = 0;
const TASK_PAGE_SIZE = 200;
type NormalizedTaskList = {
items: EditorTaskListItem[];
page: number;
size: number;
total: number;
totalPages: number;
};
const resolveSegmentIndex = (value: unknown) => {
if (value === null || value === undefined) return undefined;
@@ -135,6 +149,41 @@ const buildAnnotationSnapshot = (annotation?: Record<string, unknown>) => {
const buildSnapshotKey = (fileId: string, segmentIndex?: number) =>
`${fileId}::${segmentIndex ?? "full"}`;
const mergeTaskItems = (base: EditorTaskListItem[], next: EditorTaskListItem[]) => {
if (next.length === 0) return base;
const seen = new Set(base.map((item) => item.fileId));
const merged = [...base];
next.forEach((item) => {
if (seen.has(item.fileId)) return;
seen.add(item.fileId);
merged.push(item);
});
return merged;
};
const mergeTaskPages = (pages: EditorTaskListItem[][]) =>
pages.reduce((acc, page) => mergeTaskItems(acc, page), []);
const normalizeTaskListResponse = (
response: ApiResponse<EditorTaskListResponse> | null | undefined,
fallbackPage: number,
): NormalizedTaskList => {
const content = response?.data?.content;
const items = Array.isArray(content) ? content : [];
const size = response?.data?.size ?? TASK_PAGE_SIZE;
const total = response?.data?.totalElements ?? items.length;
const totalPages =
response?.data?.totalPages ?? (size > 0 ? Math.ceil(total / size) : 0);
const page = response?.data?.page ?? fallbackPage;
return {
items,
page,
size,
total,
totalPages,
};
};
export default function LabelStudioTextEditor() {
const { projectId = "" } = useParams();
const navigate = useNavigate();
@@ -163,6 +212,10 @@ export default function LabelStudioTextEditor() {
const [lsReady, setLsReady] = useState(false);
const [project, setProject] = useState<EditorProjectInfo | null>(null);
const [tasks, setTasks] = useState<EditorTaskListItem[]>([]);
const [taskPage, setTaskPage] = useState(TASK_PAGE_START);
const [taskTotal, setTaskTotal] = useState(0);
const [taskTotalPages, setTaskTotalPages] = useState(0);
const [loadingMore, setLoadingMore] = useState(false);
const [selectedFileId, setSelectedFileId] = useState<string>("");
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [autoSaveOnSwitch, setAutoSaveOnSwitch] = useState(false);
@@ -205,31 +258,89 @@ export default function LabelStudioTextEditor() {
}
}, [message, projectId]);
const loadTasks = useCallback(async (silent = false) => {
const updateTaskSelection = useCallback((items: EditorTaskListItem[]) => {
const defaultFileId =
items.find((item) => !item.hasAnnotation)?.fileId || items[0]?.fileId || "";
setSelectedFileId((prev) => {
if (prev && items.some((item) => item.fileId === prev)) return prev;
return defaultFileId;
});
}, []);
const loadTasks = useCallback(async (options?: {
mode?: "reset" | "append" | "refresh";
silent?: boolean;
}) => {
if (!projectId) return;
if (!silent) setLoadingTasks(true);
const mode = options?.mode ?? "reset";
const silent = options?.silent ?? false;
if (mode === "append" && taskTotalPages > 0 && taskPage + 1 >= taskTotalPages) {
return;
}
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: 0,
size: 200,
page: nextPage,
size: TASK_PAGE_SIZE,
})) as ApiResponse<EditorTaskListResponse>;
const content = resp?.data?.content || [];
const items = Array.isArray(content) ? content : [];
setTasks(items);
const defaultFileId =
items.find((item) => !item.hasAnnotation)?.fileId || items[0]?.fileId || "";
setSelectedFileId((prev) => {
if (prev && items.some((item) => item.fileId === prev)) return prev;
return defaultFileId;
});
const normalized = normalizeTaskListResponse(resp, nextPage);
const nextItems =
mode === "append" ? mergeTaskItems(tasks, normalized.items) : normalized.items;
setTasks(nextItems);
setTaskPage(normalized.page);
setTaskTotal(normalized.total);
setTaskTotalPages(normalized.totalPages);
updateTaskSelection(nextItems);
} catch (e) {
console.error(e);
if (!silent) message.error("获取文件列表失败");
setTasks([]);
if (!silent && mode !== "append") message.error("获取文件列表失败");
if (mode === "reset") {
setTasks([]);
setTaskPage(TASK_PAGE_START);
setTaskTotal(0);
setTaskTotalPages(0);
}
} finally {
if (!silent) setLoadingTasks(false);
if (mode === "append") {
setLoadingMore(false);
} else if (!silent) {
setLoadingTasks(false);
}
}
}, [message, projectId]);
}, [message, projectId, taskPage, taskTotalPages, tasks, updateTaskSelection]);
const initEditorForFile = useCallback(async (fileId: string, segmentIdx?: number) => {
if (!project?.supported) return;
@@ -397,7 +508,7 @@ export default function LabelStudioTextEditor() {
segmentIndex,
});
message.success("标注已保存");
await loadTasks(true);
await loadTasks({ mode: "refresh", silent: true });
const snapshotKey = buildSnapshotKey(String(fileId), segmentIndex);
const snapshot = buildAnnotationSnapshot(isRecord(annotation) ? annotation : undefined);
@@ -580,6 +691,10 @@ export default function LabelStudioTextEditor() {
setIframeReady(false);
setProject(null);
setTasks([]);
setTaskPage(TASK_PAGE_START);
setTaskTotal(0);
setTaskTotalPages(0);
setLoadingMore(false);
setSelectedFileId("");
initSeqRef.current = 0;
setLsReady(false);
@@ -599,7 +714,7 @@ export default function LabelStudioTextEditor() {
useEffect(() => {
if (!project?.supported) return;
loadTasks();
loadTasks({ mode: "reset" });
}, [project?.supported, loadTasks]);
useEffect(() => {
@@ -731,6 +846,25 @@ export default function LabelStudioTextEditor() {
return () => window.removeEventListener("message", handler);
}, [message, origin, saveFromExport]);
const canLoadMore = taskTotalPages > 0 && taskPage + 1 < taskTotalPages;
const loadMoreNode = canLoadMore ? (
<div className="p-2 text-center">
<Button
size="small"
loading={loadingMore}
disabled={loadingTasks}
onClick={() => loadTasks({ mode: "append" })}
>
</Button>
{taskTotal > 0 && (
<Typography.Text type="secondary" style={{ fontSize: 10, marginLeft: 8 }}>
{tasks.length}/{taskTotal}
</Typography.Text>
)}
</div>
) : null;
if (loadingProject) {
return (
<div className="h-full flex items-center justify-center">
@@ -786,7 +920,11 @@ export default function LabelStudioTextEditor() {
</Typography.Title>
</div>
<div className="flex items-center gap-2">
<Button icon={<ReloadOutlined />} loading={loadingTasks} onClick={() => loadTasks()}>
<Button
icon={<ReloadOutlined />}
loading={loadingTasks}
onClick={() => loadTasks({ mode: "reset" })}
>
</Button>
<Button
@@ -816,6 +954,7 @@ export default function LabelStudioTextEditor() {
loading={loadingTasks}
size="small"
dataSource={tasks}
loadMore={loadMoreNode}
renderItem={(item) => (
<List.Item
key={item.fileId}