错误不阻塞、弹窗进度展示
批量操作优化-进度可视化、可取消、错误容错处理
#tech / dev / frame
#type / howto
#status / growing
批量打印优化文档(错误不阻塞、弹窗进度展示 )
概述
本次优化主要解决批量打印时的用户体验问题,包括:
- 进度可视化 - 批量打印时显示加载进度
- 可取消操作 - 用户可以随时取消批量打印
- 错误容错处理 - 单条数据失败不阻塞整体流程
一、问题背景
1.1 原有问题
在批量打印场景下,原有实现存在以下问题:
- 无进度反馈:批量获取数据时,用户只能看到 loading 状态,不知道当前处理到第几条
- 无法取消:一旦开始批量打印,无法中途取消
- 单点阻塞:如果某条数据存在问题(如缺少
department字段),API 请求会报错,导致整个批量打印中断
1.2 阻塞原因分析
原代码的批量获取逻辑:
// 原代码(会阻塞)
for (const item of taskItems) {
const data = await ArchiveRetrievalApi.getArchiveRetrieval(item.id)
await fetchRetrievalPrintList(data, result, seenArchiveIds)
}
问题:当 getArchiveRetrieval 抛出异常时,由于没有 try-catch 包裹,异常会向上冒泡,导致整个 fetchData 函数中断。
二、解决方案
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ PrintButton.vue │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ handlePrint() │ │
│ │ ├── 判断是否批量打印 │ │
│ │ ├── 打开 PrintProgressDialog │ │
│ │ └── 调用 fetchData (带回调) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ usePrint.ts │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ fetchData(options) │ │
│ │ ├── onProgress: 进度回调 │ │
│ │ ├── onError: 错误回调 │ │
│ │ └── cancelToken: 取消令牌 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PrintProgressDialog.vue │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ - 显示进度条 │ │
│ │ - 显示当前/总数 │ │
│ │ - 取消按钮 │ │
│ │ - 错误列表(可折叠) │ │
│ │ - 继续打印按钮 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
三、实现细节
3.1 FetchOptions 接口设计
interface FetchOptions {
module: PrintModule // 打印模块(3=借阅, 4=销毁)
mode?: number // 打印模式(1=单个, 2=批量, 3=草稿)
id?: number // 单个打印时的ID
onProgress?: (current: number, total: number) => void // 进度回调
onError?: (error: { id: number; code: string; message: string }) => void // 错误回调
cancelToken?: { cancelled: boolean } // 取消令牌
}
3.2 进度回调实现
在 fetchData 函数中,循环处理每条数据时调用进度回调:
const fetchData = async (options: FetchOptions): Promise<ArchiveRespVO[]> => {
const { onProgress, cancelToken } = options
const taskItems = taskStore.taskInfo as ArchiveRetrievalVO[]
const total = taskItems.length
for (let i = 0; i < taskItems.length; i++) {
// 1. 检查是否取消
if (cancelToken?.cancelled) {
console.log('[usePrint] 批量打印已取消')
break
}
// 2. 更新进度(当前索引, 总数)
onProgress?.(i, total)
// 3. 获取数据...
const data = await ArchiveRetrievalApi.getArchiveRetrieval(item.id)
}
// 4. 最终进度(全部完成)
onProgress?.(taskItems.length, total)
}
3.3 取消机制实现
使用简单的 cancelToken 对象实现取消功能:
PrintButton.vue:
// 取消令牌
const cancelToken = ref<{ cancelled: boolean }>({ cancelled: false })
// 处理取消
const handleCancelPrint = () => {
cancelToken.value.cancelled = true
}
// 开始打印时重置
const handlePrint = async () => {
cancelToken.value = { cancelled: false } // 重置
await fetchData({
// ...
cancelToken: cancelToken.value
})
}
usePrint.ts:
for (let i = 0; i < taskItems.length; i++) {
// 每次循环检查取消状态
if (cancelToken?.cancelled) {
console.log('[usePrint] 批量打印已取消,当前进度:', i, '/', total)
break // 跳出循环
}
// ...
}
3.4 错误容错处理(核心优化)
之前的代码(会阻塞):
for (const item of taskItems) {
// ❌ 没有 try-catch,异常会中断整个循环
const data = await ArchiveRetrievalApi.getArchiveRetrieval(item.id)
await fetchRetrievalPrintList(data, result, seenArchiveIds)
}
优化后的代码(不阻塞):
for (let i = 0; i < taskItems.length; i++) {
const item = taskItems[i]
try {
// ✅ 用 try-catch 包裹每个 API 调用
const data = await ArchiveRetrievalApi.getArchiveRetrieval(item.id)
await fetchRetrievalPrintList(data, result, seenArchiveIds)
} catch (error: any) {
// ✅ 捕获错误,记录但不中断
const itemCode = (item as any).applyCode || `ID-${item.id}`
console.error('[usePrint] 获取数据失败, ID:', item.id, error)
// ✅ 通过回调通知上层
onError?.({
id: item.id,
code: itemCode,
message: error?.message || 'Failed to fetch data'
})
// ✅ 继续处理下一条,不 throw,不 return
}
}
关键点:
try-catch包裹每个独立的 API 调用- 捕获到错误后不抛出(不
throw) - 捕获到错误后不返回(不
return) - 只记录错误,然后让循环自然继续
四、PrintProgressDialog 组件
4.1 组件 Props 和 Emits
interface Props {
title?: string // 弹窗标题
}
const emit = defineEmits<{
(e: 'cancel'): void // 用户点击取消
(e: 'complete'): void // 用户点击继续打印
(e: 'close'): void // 用户关闭弹窗
}>()
4.2 暴露的方法
defineExpose({
open, // 打开弹窗,传入总数
updateProgress, // 更新当前进度
addError, // 添加错误记录
complete, // 标记完成
close, // 关闭弹窗
getErrors // 获取错误列表
})
4.3 状态流转
┌────────────────┐
│ 初始状态 │ visible=false
└───────┬────────┘
│ open(totalCount)
▼
┌────────────────┐
│ 加载中 │ visible=true, isCompleted=false
│ 显示进度条 │ current 从 0 递增到 total
│ 显示取消按钮 │
└───────┬────────┘
│
┌───┴───┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│ 取消 │ │ 完成 │
│ 状态 │ │ 状态 │
└────┬───┘ └────┬───┘
│ │
│ ┌─────┴─────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌─────────────┐
│ 有错误 │ │ 无错误 │
│ 显示错误列表 │ │ 显示成功 │
│ 显示继续按钮 │ │ 显示继续按钮 │
└────────────┘ └─────────────┘
4.4 错误显示
错误列表使用 el-collapse 折叠展示:
<el-collapse v-if="errors.length > 0">
<el-collapse-item>
<template #title>
<span class="text-red-500">
{{ errors.length }} item(s) failed to load (click to expand)
</span>
</template>
<div v-for="error in errors" :key="error.id">
<div>Request ID: {{ error.code }}</div>
<div>{{ error.message }}</div>
</div>
</el-collapse-item>
</el-collapse>
五、PrintButton 集成
5.1 批量打印流程
const handlePrint = async () => {
const isBatchPrint = props.mode === 2
const batchCount = getBatchPrintCount(props.module)
if (isBatchPrint && batchCount > 1) {
// 1. 打开进度弹窗
progressDialogRef.value?.open(batchCount)
// 2. 获取数据(带回调)
printData = await fetchData({
module: props.module,
mode: props.mode,
id: props.id,
onProgress: (current, _total) => {
// 更新进度
progressDialogRef.value?.updateProgress(current)
},
cancelToken: cancelToken.value,
onError: (errorInfo) => {
// 记录错误
progressDialogRef.value?.addError(errorInfo)
}
})
// 3. 检查是否取消
if (cancelToken.value.cancelled) {
return
}
// 4. 标记完成,等待用户确认
progressDialogRef.value?.complete()
pendingPrintData.value = printData
return // 等待用户点击 "Continue to Print"
}
}
5.2 事件处理
<PrintProgressDialog
ref="progressDialogRef"
@cancel="handleCancelPrint"
@complete="continuePrintAfterLoad"
@close="handleProgressClose"
/>
// 取消打印
const handleCancelPrint = () => {
cancelToken.value.cancelled = true
}
// 继续打印(用户确认后)
const continuePrintAfterLoad = async () => {
if (pendingPrintData.value) {
await continuePrintProcess(pendingPrintData.value)
}
}
// 关闭弹窗
const handleProgressClose = () => {
pendingPrintData.value = null
loading.value = false
}
六、使用示例
6.1 基本用法
<PrintButton
:module="3"
:mode="2"
text="Print in batch"
/>
6.2 用户交互流程
- 用户点击 “Print in batch” 按钮
- 弹出进度弹窗,显示 “Loading data… (0/10)”
- 进度条逐步增加:1/10, 2/10, 3/10…
- 如果第 5 条数据报错:
- 控制台打印错误日志
- 错误被记录到弹窗的 errors 数组
- 继续处理第 6, 7, 8, 9, 10 条
- 加载完成后:
- 显示 ”⚠ 9 / 10 items processed, 1 skipped”
- 显示可折叠的错误列表
- 显示 “Continue to Print (9 items)” 按钮
- 用户点击继续,打印 9 条成功的数据
七、总结
| 优化项 | 实现方式 | 效果 |
|---|---|---|
| 进度显示 | onProgress 回调 + 进度弹窗 | 用户可以看到当前处理进度 |
| 取消功能 | cancelToken 令牌 | 用户可以随时中止批量操作 |
| 错误容错 | try-catch + onError 回调 | 单条失败不影响其他数据 |
| 错误可见 | 错误列表展示 | 用户知道哪些数据有问题 |
核心原则:将批量操作中的每个子操作隔离,一个失败不影响其他。