错误不阻塞、弹窗进度展示

批量操作优化-进度可视化、可取消、错误容错处理

#tech / dev / frame #type / howto #status / growing

批量打印优化文档(错误不阻塞、弹窗进度展示 )

概述

本次优化主要解决批量打印时的用户体验问题,包括:

  1. 进度可视化 - 批量打印时显示加载进度
  2. 可取消操作 - 用户可以随时取消批量打印
  3. 错误容错处理 - 单条数据失败不阻塞整体流程

一、问题背景

1.1 原有问题

在批量打印场景下,原有实现存在以下问题:

  1. 无进度反馈:批量获取数据时,用户只能看到 loading 状态,不知道当前处理到第几条
  2. 无法取消:一旦开始批量打印,无法中途取消
  3. 单点阻塞:如果某条数据存在问题(如缺少 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
  }
}

关键点:

  1. try-catch 包裹每个独立的 API 调用
  2. 捕获到错误后不抛出(不 throw
  3. 捕获到错误后不返回(不 return
  4. 只记录错误,然后让循环自然继续

四、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 用户交互流程

  1. 用户点击 “Print in batch” 按钮
  2. 弹出进度弹窗,显示 “Loading data… (0/10)”
  3. 进度条逐步增加:1/10, 2/10, 3/10…
  4. 如果第 5 条数据报错:
    • 控制台打印错误日志
    • 错误被记录到弹窗的 errors 数组
    • 继续处理第 6, 7, 8, 9, 10 条
  5. 加载完成后:
    • 显示 ”⚠ 9 / 10 items processed, 1 skipped”
    • 显示可折叠的错误列表
    • 显示 “Continue to Print (9 items)” 按钮
  6. 用户点击继续,打印 9 条成功的数据

七、总结

优化项实现方式效果
进度显示onProgress 回调 + 进度弹窗用户可以看到当前处理进度
取消功能cancelToken 令牌用户可以随时中止批量操作
错误容错try-catch + onError 回调单条失败不影响其他数据
错误可见错误列表展示用户知道哪些数据有问题

核心原则:将批量操作中的每个子操作隔离,一个失败不影响其他。

创建于 2025/1/1 更新于 2026/5/27