批量打印:进度、取消与部分失败
批量操作优化-进度可视化、可取消、错误容错处理
批量打印:进度、取消与部分失败
这篇保留档案批量打印的历史场景:PrintButton.vue 发起任务,usePrint.ts 获取档案,PrintProgressDialog.vue 展示进度与错误。原记录中的 module=3 表示借阅、module=4 表示销毁;mode=1/2/3 分别表示单个、批量和草稿。这些是该业务的旧接口约定,不是通用打印协议。
本篇只治理取数与 UI 状态,不证明当前项目已采用下面的示例,也不保证部分数据在所有打印业务中都可以接受。
原问题与控制边界
原流程逐条调用 ArchiveRetrievalApi.getArchiveRetrieval,再通过 fetchRetrievalPrintList 聚合并用 seenArchiveIds 去重。单条异常若直接向外抛出,会中断整个取数循环。
允许部分成功时,可以逐条收集错误;认证失败、共享服务不可用或业务要求完整批次时,应停止,而不是捕获所有异常后继续。聚合单条结果也应在局部完成后再加入结果,避免半条数据已写入共享数组却又标记为失败。
进度至少区分 total、processed、成功条目和失败条目。processed 表示已得到确定处理结果的条数,失败也计入;取消不能无条件把进度设置为 total。
可运行的串行收集核心
TypeScript 示例使用原记录的自定义 cancelToken,它只在步骤边界检查取消,不是 Axios CancelToken,也不会自动中止进行中的 I/O。load 和 onProgress 由应用注入,load 应返回当前条目的完整结果,不能修改外部共享聚合数组。
export type Item = { id: number; code: string };
export type Cancellation = { cancelled: boolean };
export type Progress = { processed: number; total: number };
export async function collectPrintData<T>(
items: readonly Item[],
load: (item: Item) => Promise<T>,
cancelToken: Cancellation,
canSkip: (error: unknown) => boolean,
onProgress: (progress: Progress) => void = () => {},
) {
const tasks = items.map(item => ({ ...item }));
const data: T[] = [];
const errors: Array<{ id: number; code: string; message: string }> = [];
let processed = 0;
let cancelled = false;
onProgress({ processed, total: tasks.length });
for (const item of tasks) {
if (cancelToken.cancelled) { cancelled = true; break; }
try {
const value = await load(item);
if (cancelToken.cancelled) { cancelled = true; break; }
data.push(value);
} catch (error) {
if (cancelToken.cancelled) { cancelled = true; break; }
if (!canSkip(error)) throw error;
// 不向 UI 回显完整下游异常、连接串或私密数据。
errors.push({ id: item.id, code: item.code, message: '该条数据加载失败' });
}
processed += 1;
onProgress({ processed, total: tasks.length });
}
return { data, errors, processed, total: tasks.length, cancelled: cancelled || cancelToken.cancelled };
}
canSkip 必须按应用错误契约分类,不能一律返回 true。onProgress 属于 UI 通知回调,应不抛错;其异常不应被误分类为某条档案取数失败。此例按源条目计数,若一条档案生成多张打印记录,源条目进度和打印页数应分别展示。
取消与重入
- 一次运行创建一个 token,并把它保存在本次运行的局部变量中。不要中途换用另一轮的全局 token。
- 运行期间禁用再次发起,或按 run ID 隔离状态;旧任务晚到的回调不能覆盖新任务。
- 自定义布尔 token 只阻止下一步和晚到结果的提交。若要取消 I/O,需要底层 API 接收 AbortSignal 或等效机制;等待应另有时间预算。
- 用户取消、关闭或组件卸载时清空待打印数据,不能在后台完成后自动打印。
- 如保留取消前的部分数据,应单独提供明确的“打印已加载部分”操作,不能沿用取消前的默认继续行为。
进度弹窗的职责
原 PrintProgressDialog 暴露 open、updateProgress、addError、complete、close、getErrors,并发送 cancel、complete、close 事件。open/complete 方法和同名事件属于应用自定义接口,接入时应区分“取数完成”与“用户确认打印”。
idle → loading
loading → cancelled / failed / ready
ready → printing(只有明确确认且存在可打印数据)
printing → submitted / failed / outcome_unknown
10 条源记录中成功 9 条、失败 1 条时,应显示“已处理 10/10,成功 9,失败 1”,而不是“处理 9/10”。空批次或全部失败不能给出看似可用的继续打印按钮。
继续打印的单次消费
下面是 UI handler 的局部模式,数据类型和实际打印函数由应用提供。先同步消费 pending 数据,再异步调用,避免双击重复提交:
export function createPrintAction<T>(submit: (data: readonly T[]) => Promise<void>) {
let pending: readonly T[] | null = null;
let printing = false;
return {
setPending(data: readonly T[]) {
if (printing) throw new Error('Print already running');
pending = [...data];
},
clear() { pending = null; },
async confirm() {
if (printing || !pending?.length) return;
const data = pending;
pending = null;
printing = true;
try { await submit(data); }
finally { printing = false; }
},
};
}
submit 失败时由调用方展示结果;不自动放回 pending 并重试,因为打印请求可能已经送达。若需要防重复打印,应由业务/打印服务提供稳定作业标识与查询机制,前端按钮锁不能保证外部副作用只发生一次。
验证
- 一条可跳过错误不会丢掉其后记录,成功数 + 失败数等于 processed。
- 不可跳过错误停止,UI 进入 failed 并清理旧 pending 数据。
- 开始前或 await 期间取消,不再接受晚到结果,也不虚报 100%。
- 重复确认只消费当前待打印批次一次;打印失败不自动重放。
- 关闭、卸载和新运行不会误用上一轮的数据与进度。