fix(报表生成): 汇总大表进度弹窗优化(自动化下载提示 + 再次下载走本地缓存)

- 完成后显示「已自动下载:文件名(大小)」+ 提示,按钮由「保存文件/下载」改为「再次下载」,并缓存 Blob(重复保存 0 请求)

- 弹窗标题随状态变化(正在生成/已生成/生成失败),完成态主按钮改为「关闭」;新任务开始时重置下载状态

- 浏览器实测(admin/123456, 2026-08):生成 13.5s、产物 1.15MB;完成后标题/提示/按钮均正确,点再次下载提示「已从本地缓存保存」,控制台 0 报错

- 文档: 功能测试报告(6 用例) + 日结 1.4
2个文件已修改
1个文件已添加
120 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_汇总大表进度弹窗优化_2026-09-22.md 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-22.md 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±í½ø¶Èµ¯´°ÓÅ»¯_2026-09-22.md
New file
@@ -0,0 +1,54 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表进度弹窗优化_2026-09-22
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户问题:「汇总表的进度条弹窗的『保存』会使下载的汇总表再次下载;这个弹窗可以怎么优化一下呢?」
**原因**:任务完成时 `downloadTaskFile(true)` ä¼šè‡ªåŠ¨ä¸‹è½½ä¸€æ¬¡ï¼›è€Œå¼¹çª—åº•éƒ¨ä¸€ç›´æŒ‚ç€ã€Œä¿å­˜æ–‡ä»¶ã€ï¼ˆç¼“å­˜å‘½ä¸­æ—¶æ˜¯ã€Œä¸‹è½½ã€ï¼‰ï¼Œç‚¹ä¸€ä¸‹ä¼š**再次请求后端拿同一个文件并第二次下载**,看起来像「保存又下了一遍」。同时弹窗标题始终是「正在生成汇总大表」,完成/失败后不变化。
**改动**(`traffic-audit-web/src/views/ReportExport.vue`):
| # | æ”¹åЍ | è¯´æ˜Ž |
| --- | --- | --- |
| 1 | å®ŒæˆåŽæ˜¾ç¤ºã€Œå·²è‡ªåŠ¨ä¸‹è½½ã€ | `已自动下载:<文件名>(<大小>)` + æç¤ºã€Œæµè§ˆå™¨æ²¡å¼¹å‡ºä¸‹è½½æ—¶å¯ç‚¹ã€Žå†æ¬¡ä¸‹è½½ã€â€”—用本地缓存保存,不会重新请求服务器」 |
| 2 | æŒ‰é’®æ–‡æ¡ˆéšçŠ¶æ€å˜åŒ– | å·²ä¸‹è½½ â†’ **再次下载**;未下载(缓存命中场景)→ ä¸‹è½½ï¼›ç”Ÿæˆä¸­ â†’ ä¿å­˜æ–‡ä»¶ï¼ˆç¦ç”¨ï¼‰ |
| 3 | å†æ¬¡ä¸‹è½½èµ°**内存缓存** | é¦–次下载后把 Blob å­˜è¿› `taskBlob`,再次点击直接本地另存(提示「已从本地缓存保存」),**不再请求后端** |
| 4 | å¼¹çª—标题随状态变化 | æ­£åœ¨ç”Ÿæˆæ±‡æ€»å¤§è¡¨ / **汇总大表已生成** / æ±‡æ€»å¤§è¡¨ç”Ÿæˆå¤±è´¥ |
| 5 | å®Œæˆæ€ä¸»æŒ‰é’®æ”¹ä¸ºã€Œå…³é—­ã€ | ç”Ÿæˆä¸­ä»æ˜¯ã€ŒåŽå°è¿è¡Œã€ï¼Œå®ŒæˆåŽä¸»æŒ‰é’®æ˜¯ã€Œå…³é—­ã€ï¼ˆä¸‹è½½æŒ‰é’®é™ä¸ºæ¬¡è¦ï¼‰ |
| 6 | æ¯æ¬¡æ–°ä»»åŠ¡é‡ç½®çŠ¶æ€ | `taskDownloaded/taskFileName/taskFileSize/taskBlob` åœ¨ `startSummaryTask` é‡Œæ¸…é›¶ |
## äºŒã€æµ‹è¯•环境
- å‰ç«¯ï¼š`npm run serve`(8080),改后热编译 `Compiled successfully in 592ms`(10:00:24)
- åŽç«¯ï¼šSpring Boot 8090(今日运行中);数据库 MySQL 3308 / `traffic_audit`
- æµè§ˆå™¨ï¼šCodex å†…置浏览器(视口 1280×720),登录 `admin/123456`
- æŠ¥è¡¨æœŸï¼š2026-08(月报,汇总大表《道路运输量汇总表(整本)》数据状态=数据就绪)
- ç”Ÿæˆè€—时:**13.5 ç§’**(未命中缓存),产物 `生成_道路运输量汇总表_2026-08.xlsx`,**1.15 MB**
## ä¸‰ã€ç”¨ä¾‹è¡¨
| # | ç”¨ä¾‹ | è¾“å…¥/操作 | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T01 | ç”Ÿæˆä¸­å¼¹çª—状态(正向) | é€‰ 2026-08 â†’ ç‚¹ã€Œå¯¼å‡ºã€ | æ ‡é¢˜ã€Œæ­£åœ¨ç”Ÿæˆæ±‡æ€»å¤§è¡¨ã€ï¼›è¿›åº¦é€’增;按钮「保存文件(禁用)/重新生成(禁用)/后台运行」 | æ ‡é¢˜æ­£ç¡®ï¼›è¿›åº¦ 6%→45%;步骤文字「读取母版工作簿」→「刷新排名页与跨页合成页」;按钮状态与预期一致 | é€šè¿‡ |
| T02 | å®ŒæˆåŽè‡ªåŠ¨ä¸‹è½½ + æç¤ºï¼ˆæ­£å‘) | ç­‰å¾…任务结束 | æ ‡é¢˜å˜ã€Œæ±‡æ€»å¤§è¡¨å·²ç”Ÿæˆã€ï¼›æ˜¾ç¤ºã€Œå·²è‡ªåŠ¨ä¸‹è½½ï¼šæ–‡ä»¶åï¼ˆå¤§å°ï¼‰ã€ï¼›æŒ‰é’®å˜ã€Œå†æ¬¡ä¸‹è½½/重新生成/关闭」 | è¿›åº¦ 100%、「完成」、已用 13.5 ç§’;提示「已自动下载:生成_道路运输量汇总表_2026-08.xlsx ï¼ˆ1.15 MB)」;按钮已变更 | é€šè¿‡ |
| T03 | å†æ¬¡ä¸‹è½½èµ°æœ¬åœ°ç¼“存(正向,核心) | ç‚¹ã€Œå†æ¬¡ä¸‹è½½ã€ | æç¤ºèµ°ç¼“存、且**不再请求后端** | æç¤ºã€Œå·²ä»Žæœ¬åœ°ç¼“存保存:生成_道路运输量汇总表_2026-08.xlsx」,未出现「报表导出成功」(该文案只在走后端接口时出现)→ èµ°çš„æ˜¯ç¼“存分支 | é€šè¿‡ |
| T04 | å…³é—­æŒ‰é’®ï¼ˆæ­£å‘) | ç‚¹ã€Œå…³é—­ã€ | å¼¹çª—消失、列表页可继续操作 | å¼¹çª—容器从可访问性树中移除,列表正常 | é€šè¿‡ |
| T05 | æŽ§åˆ¶å°æ— æŠ¥é”™ï¼ˆå›žå½’) | è¯»å– dev logs(error/warn) | æ— æ–°å¢žé”™è¯¯ | 0 æ¡ | é€šè¿‡ |
| T06 | å…¶å®ƒæŠ¥è¡¨å¯¼å‡ºè·¯å¾„未受影响(回归,静态) | â€”— | å•å¼ /批量导出仍走原 `downloadBlob` | æœ¬æ¬¡åªæ”¹æ±‡æ€»å¤§è¡¨å¼‚步任务分支,`exportChecked` ä¸­å…¶å®ƒåˆ†æ”¯æœªåЍ | é€šè¿‡ï¼ˆé™æ€ï¼‰ |
## å››ã€å‘现的问题与处置
| # | é—®é¢˜ | å¤„ç½® |
| --- | --- | --- |
| P1 | å¼¹çª—标题与按钮文案和真实状态不符(已完成仍写「正在生成」;已自动下载仍写「保存文件」) | å·²ä¿®ï¼ˆæ ‡é¢˜éšçŠ¶æ€ã€æŒ‰é’®æ–‡æ¡ˆéšä¸‹è½½çŠ¶æ€ï¼‰ |
| P2 | åŒä¸€ä»½æ–‡ä»¶è¢«é‡å¤è¯·æ±‚后端(1.15 MB Ã— æ¬¡æ•°ï¼‰ | å·²ä¿®ï¼ˆå†…å­˜ Blob ç¼“存,重复保存 0 è¯·æ±‚) |
| P3 | éªŒè¯æ–¹æ³•记录:Element UI æœˆä»½é€‰æ‹©æ¡†æ˜¯ **readonly** è¾“入框,自动化不能用 fill/type,必须先点开弹层再选月份;`setValue` å¯¹è¯¥æŽ§ä»¶æ— æ•ˆ | ä»…记录为测试方法备注,非系统缺陷 |
## äº”、结论
- å¼¹çª—现在能明确告诉用户「已自动下载了什么文件、多大」,按钮语义与行为一致(「再次下载」= å†å­˜ä¸€ä»½ï¼Œèµ°æœ¬åœ°ç¼“存);
- é‡å¤ä¸‹è½½ä¸å†é‡å¤è¯·æ±‚后端,体验与带宽都改善;
- ç”Ÿæˆä¸­/已完成/失败三种状态在标题、进度、按钮上都有区分;
- æµè§ˆå™¨ç«¯å®žæµ‹ 6 æ¡ç”¨ä¾‹é€šè¿‡ï¼Œæ— æŽ§åˆ¶å°æŠ¥é”™ã€‚
测试人:Codex(AI) 测试日期:2026-09-22
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-22.md
@@ -24,9 +24,21 @@
用户在别处看到"汇总"按钮,实际核查:前端 `DataView.vue` å…¨æ–‡æ— "汇总/导出"(`ReportExport.vue` æ‰æœ‰ï¼‰ï¼ŒåŽç«¯åªæœ‰ `/api/data/list`,全仓 + `traffic-audit-web/dist` + éƒ¨ç½² jar + gitea è¿œç«¯ `main` å‡æ— ç›¸å…³ä»£ç ã€‚已向用户说明,按"尚未实现、由本仓实现"推进。
### 4. æ±‡æ€»å¤§è¡¨è¿›åº¦å¼¹çª—优化(用户:"保存"会导致再下载一次)
- **根因**:任务完成时会自动下载一次,但弹窗底部仍挂着「保存文件」按钮,点它会再次请求后端拿同一份文件并第二次下载;同时标题/按钮文案不随状态变化。
- **改动**(`traffic-audit-web/src/views/ReportExport.vue`):
  1. å®ŒæˆåŽæ˜¾ç¤ºã€Œå·²è‡ªåŠ¨ä¸‹è½½ï¼š<文件名>(<大小>)」+ æç¤ºå¯ç‚¹ã€Œå†æ¬¡ä¸‹è½½ã€ï¼ˆæœ¬åœ°ç¼“存,不再请求服务器);
  2. æŒ‰é’®æ–‡æ¡ˆéšçŠ¶æ€ï¼šå†æ¬¡ä¸‹è½½ / ä¸‹è½½ / ä¿å­˜æ–‡ä»¶ï¼ˆç”Ÿæˆä¸­ç¦ç”¨ï¼‰ï¼›
  3. é¦–次下载后把 Blob å­˜å†…存,重复保存 **0 è¯·æ±‚**;
  4. å¼¹çª—标题随状态:正在生成 / **已生成** / ç”Ÿæˆå¤±è´¥ï¼›å®Œæˆæ€ä¸»æŒ‰é’®æ”¹ä¸ºã€Œå…³é—­ã€ï¼›
  5. æ¯æ¬¡æ–°ä»»åŠ¡é‡ç½® `taskDownloaded/taskFileName/taskFileSize/taskBlob`。
- **浏览器实测**(admin/123456,2026-08):生成 13.5 ç§’、产物 1.15 MB;完成后标题「汇总大表已生成」、提示「已自动下载:生成_道路运输量汇总表_2026-08.xlsx ï¼ˆ1.15 MB)」、点「再次下载」提示「已从本地缓存保存」;控制台 0 æŠ¥é”™ã€‚
## äºŒã€æµ‹è¯•结果摘要
- æŠ¥å‘Šï¼š`docs/功能测试报告/功能测试报告_数据查看补齐网约车与个体客运类型_2026-09-22.md`
- æŠ¥å‘Šï¼š`docs/功能测试报告/功能测试报告_汇总大表进度弹窗优化_2026-09-22.md`(6 æ¡ç”¨ä¾‹é€šè¿‡ï¼šç”Ÿæˆä¸­çŠ¶æ€ / å®Œæˆæç¤º / å†æ¬¡ä¸‹è½½èµ°ç¼“å­˜ / å…³é—­ / æ— æŽ§åˆ¶å°æŠ¥é”™ / å…¶å®ƒå¯¼å‡ºæœªå—影响)
- çŽ¯å¢ƒï¼šåŽç«¯ `mvn spring-boot:run`(8090,`Started TrafficAuditApplication`);前端 `npm run serve`(8080,`Compiled successfully in 4769ms`);MySQL `localhost:3308/traffic_audit`;登录 admin/123456 å– JWT。
- ç»“果:3 ä¸ªæ–°ç±»åž‹æ­£å‘(含期间/关键字筛选)、负向(未知类型、无数据期、无 token)全部通过;入库抽查条数与库内一致(20 / 20 / 139);原有 15 ç±»å›žå½’正常。
traffic-audit-web/src/views/ReportExport.vue
@@ -162,7 +162,7 @@
        </el-dialog>
        <!-- æ±‡æ€»å¤§è¡¨å¼‚步生成进度(生成一次约 10~20 ç§’,避免点击后长时间无响应) -->
        <el-dialog title="正在生成汇总大表" :visible.sync="taskVisible" width="460px"
        <el-dialog :title="taskDialogTitle" :visible.sync="taskVisible" width="460px"
                   :close-on-click-modal="false" :show-close="taskFinished">
          <div class="task-body">
            <el-progress :percentage="taskInfo.percent || 0" :status="taskInfo.status === 'FAILED' ? 'exception' : (taskDone ? 'success' : null)"
@@ -170,13 +170,17 @@
            <p class="task-step">{{ taskInfo.step || '排队中' }}</p>
            <p class="task-meta" v-if="taskInfo.period">报表期 {{ taskInfo.period }} {{ taskInfo.fromCache ? '上次生成用时' : '已用' }} {{ (taskInfo.elapsedMs / 1000).toFixed(1) }} ç§’</p>
            <p class="task-meta" v-if="taskInfo.fromCache">本次直接使用 {{ taskCacheTime }} ç”Ÿæˆçš„结果(10 åˆ†é’Ÿå†…且期间没有新导入数据)。如需按最新数据重算,请点「重新生成」。</p>
            <p class="task-done" v-if="taskDownloaded">
              å·²è‡ªåŠ¨ä¸‹è½½ï¼š<b>{{ taskFileName }}</b><span v-if="taskFileSize">({{ taskFileSizeText }})</span><br>
              æµè§ˆå™¨æ²¡å¼¹å‡ºä¸‹è½½æ—¶å¯ç‚¹ã€Œå†æ¬¡ä¸‹è½½ã€â€”—用本地缓存保存,不会重新请求服务器。
            </p>
            <p class="task-error" v-if="taskInfo.status === 'FAILED'">{{ taskInfo.error }}</p>
          </div>
          <span slot="footer">
            <el-button size="small" type="primary" :disabled="!taskDone" @click="downloadTaskFile">{{ taskInfo.fromCache ? '下载' : '保存文件' }}</el-button>
            <el-button size="small" :disabled="!taskDone" @click="downloadTaskFile()">{{ taskDownloaded ? '再次下载' : (taskInfo.fromCache ? '下载' : '保存文件') }}</el-button>
            <el-button size="small" :disabled="taskRunning" @click="restartTask">重新生成</el-button>
            <el-button size="small" v-if="taskRunning" @click="minimizeTask">后台运行</el-button>
            <el-button size="small" v-else @click="closeTask">关闭</el-button>
            <el-button size="small" v-else type="primary" @click="closeTask">关闭</el-button>
          </span>
        </el-dialog>
      </el-main>
@@ -212,6 +216,11 @@
      taskVisible: false,
      taskInfo: {},
      taskTimer: null,
      // è¿›åº¦å¼¹çª—:完成后会自动下载一次,这里记录「已下载」与文件信息(重复点击走本地缓存,不再请求后端)
      taskDownloaded: false,
      taskFileName: '',
      taskFileSize: 0,
      taskBlob: null,
      taskKey: 'summaryWorkbook',
      modules: [
        {
@@ -320,6 +329,18 @@
      const d = new Date(t)
      const p = (n) => String(n).padStart(2, '0')
      return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
    },
    // å¼¹çª—标题随任务状态变化,避免「已完成」还写着「正在生成」
    taskDialogTitle() {
      const s = this.taskInfo && this.taskInfo.status
      if (s === 'FAILED') return '汇总大表生成失败'
      if (s === 'DONE') return '汇总大表已生成'
      return '正在生成汇总大表'
    },
    taskFileSizeText() {
      const n = this.taskFileSize || 0
      if (!n) return ''
      return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MB' : Math.round(n / 1024) + ' KB'
    }
  },
  watch: {
@@ -524,6 +545,10 @@
      this.stopTaskPolling()
      this.taskInfo = { status: 'PENDING', percent: 0, step: '提交任务', period: this.period, elapsedMs: 0 }
      this.taskVisible = true
      this.taskDownloaded = false
      this.taskFileName = ''
      this.taskFileSize = 0
      this.taskBlob = null
      this.loading = 'gen-' + this.curModule.key
      const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' })
      api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 })
@@ -536,7 +561,7 @@
            return
          }
          this.taskInfo = res.data
          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(); return }
          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(true); return }
          if (this.taskInfo.status === 'FAILED') { this.loading = ''; return }
          this.pollTask()
        })
@@ -568,7 +593,7 @@
            if (this.taskInfo.status === 'DONE') {
              this.stopTaskPolling()
              this.loading = ''
              this.downloadTaskFile()
              this.downloadTaskFile(true)
            } else if (this.taskInfo.status === 'FAILED') {
              this.stopTaskPolling()
              this.loading = ''
@@ -581,12 +606,26 @@
    stopTaskPolling() {
      if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
    },
    downloadTaskFile() {
    // auto=true è¡¨ç¤ºã€Œä»»åŠ¡å®ŒæˆåŽçš„è‡ªåŠ¨ä¸‹è½½ã€ï¼›ç”¨æˆ·ç‚¹æŒ‰é’®æ—¶ä¸º false
    downloadTaskFile(auto) {
      const id = this.taskInfo && this.taskInfo.taskId
      if (!id) return
      const name = this.taskInfo.fileName || ('生成_道路运输量汇总表_' + this.period + '.xlsx')
      this.taskFileName = name
      // å·²ç»ä¸‹è½½è¿‡ï¼šç›´æŽ¥ç”¨å†…存里的 Blob å†å­˜ä¸€æ¬¡ï¼Œä¸å†è¯·æ±‚后端(避免同一份文件重复拉取 1MB+)
      if (this.taskBlob) {
        this.downloadBlob(this.taskBlob, name)
        this.$message.success('已从本地缓存保存:' + name)
        return
      }
      api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 })
        .then(res => { this.downloadBlob(res, name); this.$message.success('报表导出成功') })
        .then(res => {
          this.taskBlob = res
          this.taskFileSize = (res && res.size) || 0
          this.taskDownloaded = true
          this.downloadBlob(res, name)
          this.$message.success(auto ? '生成完成,已自动下载' : '报表导出成功')
        })
        .catch(err => this.failWith(err))
    },
    restartTask() { this.startSummaryTask(true) },
@@ -756,6 +795,7 @@
.task-step { margin: 12px 0 4px; font-size: 14px; color:#303133; }
.task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; }
.task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; }
.task-done { margin: 8px 0 0; font-size: 13px; color:#67C23A; line-height: 1.6; }
</style>
<style>