From 4eee579aa38af2ac9b48e4f6e6cfc1bedebf4822 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 22 九月 2026 10:04:48 +0800
Subject: [PATCH] fix(报表生成): 汇总大表进度弹窗优化(自动化下载提示 + 再次下载走本地缓存)

---
 traffic-audit-web/src/views/ReportExport.vue |  201 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 188 insertions(+), 13 deletions(-)

diff --git a/traffic-audit-web/src/views/ReportExport.vue b/traffic-audit-web/src/views/ReportExport.vue
index 8448ec1..cdebf87 100644
--- a/traffic-audit-web/src/views/ReportExport.vue
+++ b/traffic-audit-web/src/views/ReportExport.vue
@@ -160,6 +160,29 @@
             <el-button size="small" type="primary" @click="saveNote">淇濆瓨</el-button>
           </span>
         </el-dialog>
+
+        <!-- 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愯繘搴︼紙鐢熸垚涓�娆$害 10~20 绉掞紝閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴旓級 -->
+        <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)"
+                         :stroke-width="14"></el-progress>
+            <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" :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 type="primary" @click="closeTask">鍏抽棴</el-button>
+          </span>
+        </el-dialog>
       </el-main>
     </el-container>
   </el-container>
@@ -189,6 +212,16 @@
       noteDialogVisible: false,
       noteEditingKey: '',
       noteDraft: '',
+      // 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愪换鍔�
+      taskVisible: false,
+      taskInfo: {},
+      taskTimer: null,
+      // 杩涘害寮圭獥锛氬畬鎴愬悗浼氳嚜鍔ㄤ笅杞戒竴娆★紝杩欓噷璁板綍銆屽凡涓嬭浇銆嶄笌鏂囦欢淇℃伅锛堥噸澶嶇偣鍑昏蛋鏈湴缂撳瓨锛屼笉鍐嶈姹傚悗绔級
+      taskDownloaded: false,
+      taskFileName: '',
+      taskFileSize: 0,
+      taskBlob: null,
+      taskKey: 'summaryWorkbook',
       modules: [
         {
           key: 'summary', title: '姹囨�诲ぇ琛�', items: [
@@ -277,6 +310,38 @@
         options: months.map(mo => ({ label: y + '骞�' + mo + '鏈�', value: y + '-' + String(mo).padStart(2, '0') }))
       }))
     },
+    // ===== 浠ヤ笅涓烘眹鎬诲ぇ琛ㄥ紓姝ョ敓鎴愬脊绐楃殑娲剧敓鐘舵�侊紙蹇呴』骞跺叆鍚屼竴涓� computed锛屽彟璧峰悓鍚� key 浼氳鐩栨帀涓婇潰鐨勮绠楀睘鎬э級=====
+    taskRunning() {
+      const s = this.taskInfo && this.taskInfo.status
+      return s === 'PENDING' || s === 'RUNNING'
+    },
+    taskDone() {
+      return this.taskInfo && this.taskInfo.status === 'DONE'
+    },
+    // 鐢熸垚缁撴潫锛堟垚鍔熸垨澶辫触锛夊悗鎵嶅厑璁告敹璧峰脊绐�
+    taskFinished() {
+      const s = this.taskInfo && this.taskInfo.status
+      return s === 'DONE' || s === 'FAILED'
+    },
+    taskCacheTime() {
+      const t = this.taskInfo && this.taskInfo.finishedAt
+      if (!t) return '涓婃'
+      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 '姝e湪鐢熸垚姹囨�诲ぇ琛�'
+    },
+    taskFileSizeText() {
+      const n = this.taskFileSize || 0
+      if (!n) return ''
+      return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MB' : Math.round(n / 1024) + ' KB'
+    }
   },
   watch: {
     activeKey() {
@@ -289,7 +354,11 @@
     this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0]
     this.ensureCycle()
     this.refreshAll()
-  },  methods: {
+  },
+  beforeDestroy() {
+    this.stopTaskPolling()
+  },
+  methods: {
     moduleTitle(key) {
       const m = this.modules.find(x => x.key === key)
       return m ? m.title : key
@@ -420,28 +489,28 @@
         {
           name: '閬撹矾璐ц繍',
           rows: mk([
-            ['褰撴湀璐ц繍閲�(涓囧惃)', '鍏ㄧ渷璐ц繍閲徛峰綋鏈�'],
-            ['褰撴湀璐х墿鍛ㄨ浆閲�(涓囧惃)', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛峰綋鏈�', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛峰綋鏈堝悓姣�'],
-            ['绱璐ц繍閲�(涓囧惃)', '鍏ㄧ渷璐ц繍閲徛风疮璁�'],
+            ['褰撴湀璐ц繍閲�(涓囧惃)', '鍏ㄧ渷璐ц繍閲徛峰綋鏈�', '鍏ㄧ渷璐ц繍閲徛峰綋鏈堝悓姣�'],
+            ['褰撴湀璐х墿鍛ㄨ浆閲�(涓囧惃鍏噷)', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛峰綋鏈�', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛峰綋鏈堝悓姣�'],
+            ['绱璐ц繍閲�(涓囧惃)', '鍏ㄧ渷璐ц繍閲徛风疮璁�', '鍏ㄧ渷璐ц繍閲徛风疮璁″悓姣�'],
             ['绱璐х墿鍛ㄨ浆閲�(涓囧惃鍏噷)', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛风疮璁�', '鍏ㄧ渷璐х墿鍛ㄨ浆閲徛风疮璁″悓姣�']
           ])
         },
         {
           name: '鍩庡競瀹㈣繍',
           rows: mk([
-            ['褰撴湀鍩庡競瀹㈣繍閲�:', '鍩庡競瀹㈣繍閲徛峰綋鏈�'],
-            ['褰撴湀鍩庡競瀹㈣繍鍛ㄨ浆閲�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛峰綋鏈�'],
-            ['绱鍩庡競瀹㈣繍閲�:', '鍩庡競瀹㈣繍閲徛风疮璁�'],
-            ['绱鍩庡競瀹㈣繍鍛ㄨ浆閲�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛风疮璁�']
+            ['褰撴湀鍩庡競瀹㈣繍閲�', '鍩庡競瀹㈣繍閲徛峰綋鏈�', '鍩庡競瀹㈣繍閲徛峰綋鏈堝悓姣�'],
+            ['褰撴湀鍩庡競瀹㈣繍鍛ㄨ浆閲�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛峰綋鏈�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛峰綋鏈堝悓姣�'],
+            ['绱鍩庡競瀹㈣繍閲�', '鍩庡競瀹㈣繍閲徛风疮璁�', '鍩庡競瀹㈣繍閲徛风疮璁″悓姣�'],
+            ['绱鍩庡競瀹㈣繍鍛ㄨ浆閲�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛风疮璁�', '鍩庡競瀹㈣繍鍛ㄨ浆閲徛风疮璁″悓姣�']
           ])
         },
         {
           name: '涓彛寰�',
           rows: mk([
-            ['褰撴湀涓彛寰勫杩愰噺', '涓彛寰勫杩愰噺路褰撴湀'],
-            ['褰撴湀涓彛寰勫懆杞噺', '涓彛寰勫懆杞噺路褰撴湀'],
-            ['绱涓彛寰勫杩愰噺', '涓彛寰勫杩愰噺路绱'],
-            ['绱涓彛寰勫懆杞噺', '涓彛寰勫懆杞噺路绱']
+            ['褰撴湀涓彛寰勫杩愰噺', '涓彛寰勫杩愰噺路褰撴湀', '涓彛寰勫杩愰噺路褰撴湀鍚屾瘮'],
+            ['褰撴湀涓彛寰勫懆杞噺', '涓彛寰勫懆杞噺路褰撴湀', '涓彛寰勫懆杞噺路褰撴湀鍚屾瘮'],
+            ['绱涓彛寰勫杩愰噺', '涓彛寰勫杩愰噺路绱', '涓彛寰勫杩愰噺路绱鍚屾瘮'],
+            ['绱涓彛寰勫懆杞噺', '涓彛寰勫懆杞噺路绱', '涓彛寰勫懆杞噺路绱鍚屾瘮']
           ])
         }
       ]
@@ -468,6 +537,104 @@
       if (continuous) return '1~' + cov[cov.length - 1] + '鏈堝潎鏈夋暟鎹�'
       return cov.map(x => x + '鏈�').join('銆�') + ' 鏈夋暟鎹�'
     },
+    // ==================== 姹囨�诲ぇ琛ㄥ紓姝ョ敓鎴愶紙杩涘害寮圭獥锛� ====================
+    // 鍚庣 ReportExportTaskService锛氭彁浜や换鍔� -> 杞杩涘害 -> 瀹屾垚鍚庝笅杞芥枃浠躲��
+    // 鍚屾姤琛ㄦ湡 10 鍒嗛挓鍐呬笖鏈熼棿娌℃湁鏂板鍏ユ暟鎹椂锛屽悗绔洿鎺ヨ繑鍥炰笂娆$粨鏋滐紙寮圭獥鍐呬細璇存槑鏉ユ簮鏃堕棿锛夈��
+    startSummaryTask(refresh) {
+      if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鍛ㄦ湡鍜屾湀浠�'); return }
+      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 })
+        .then(res => {
+          // 鍚庣缁熶竴杩斿洖 Result锛歝ode 闈� 200 鏃� data 涓虹┖锛屽繀椤绘彁绀鸿�屼笉鏄┖杞�
+          if (!res || res.code !== 200 || !res.data || !res.data.taskId) {
+            this.taskInfo = { status: 'FAILED', percent: 0, step: '鎻愪氦澶辫触', period: this.period, elapsedMs: 0,
+                              error: (res && res.message) || '鍚庣鏈繑鍥炰换鍔″彿' }
+            this.loading = ''
+            return
+          }
+          this.taskInfo = res.data
+          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(true); return }
+          if (this.taskInfo.status === 'FAILED') { this.loading = ''; return }
+          this.pollTask()
+        })
+        .catch(err => {
+          this.stopTaskPolling()
+          this.loading = ''
+          this.taskVisible = false
+          this.failWith(err, '鐢熸垚')
+        })
+    },
+    pollTask() {
+      this.stopTaskPolling()
+      const id = this.taskInfo && this.taskInfo.taskId
+      if (!id) return
+      this.taskTimer = setInterval(() => {
+        api.get('/report/exportTask/' + id, { timeout: 30000 })
+          .then(res => {
+            if (!res || res.code !== 200 || !res.data) {
+              // 浠诲姟琚竻鐞�/杩囨湡锛氬仠姝㈣疆璇㈠苟缁欏嚭鍘熷洜锛屼笉璁╁脊绐椾竴鐩寸┖杞�
+              this.stopTaskPolling()
+              this.loading = ''
+              this.taskInfo = Object.assign({}, this.taskInfo, {
+                status: 'FAILED', step: '浠诲姟宸插け鏁�',
+                error: (res && res.message) || '瀵煎嚭浠诲姟宸插け鏁堬紝璇烽噸鏂扮敓鎴�'
+              })
+              return
+            }
+            this.taskInfo = res.data
+            if (this.taskInfo.status === 'DONE') {
+              this.stopTaskPolling()
+              this.loading = ''
+              this.downloadTaskFile(true)
+            } else if (this.taskInfo.status === 'FAILED') {
+              this.stopTaskPolling()
+              this.loading = ''
+              if (!this.taskVisible) this.$message.error('姹囨�诲ぇ琛ㄧ敓鎴愬け璐ワ細' + (this.taskInfo.error || '鏈煡鍘熷洜'))
+            }
+          })
+          .catch(() => { /* 鍗曟杞澶辫触蹇界暐锛屼笅涓�杞户缁� */ })
+      }, 700)
+    },
+    stopTaskPolling() {
+      if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
+    },
+    // 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.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) },
+    closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' },
+    // 鐢熸垚涓敹璧峰脊绐楋細浠诲姟缁х画鍦ㄥ悗鍙版墽琛岋紝瀹屾垚鍚庝粛浼氳嚜鍔ㄤ笅杞斤紙涓庡師鏉ュ悓姝ュ鍑虹殑銆岀偣瀹屽嵆涓嬭浇銆嶄竴鑷达級
+    minimizeTask() {
+      this.taskVisible = false
+      this.$message.info('宸茶浆鍏ュ悗鍙扮敓鎴愶紝瀹屾垚鍚庝細鑷姩涓嬭浇')
+    },
     exportChecked(itemList) {
       const m = this.curModule
       if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鍛ㄦ湡鍜屾湀浠�'); return }
@@ -477,6 +644,8 @@
       const params = this.buildParams()
       const doExport = () => {
         if (items.length === 1) {
+          // 姹囨�诲ぇ琛ㄧ敓鎴愪竴娆$害 10~20 绉掞紝璧板紓姝ヤ换鍔★紙杩涘害寮圭獥锛夛紝閬垮厤鐐瑰嚮鍚庨暱鏃堕棿鏃犲搷搴�
+          if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return }
           api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
             .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('鎶ヨ〃瀵煎嚭鎴愬姛'); this.loading = '' })
             .catch(err => this.failWith(err))
@@ -621,6 +790,12 @@
 .wb-yoy { text-align:right; color:#606266; }
 .wb-yoy.yoy-empty { color:#C0C4CC; }
 .wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; }
+/* 寮傛鐢熸垚杩涘害寮圭獥 */
+.task-body { padding: 4px 2px; }
+.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>
@@ -629,4 +804,4 @@
 .pv-dialog { border-radius: 10px; overflow: hidden; }
 .pv-dialog .el-dialog__header { cursor: move; user-select: none; -webkit-user-select: none; }
 .pv-dialog .el-dialog__headerbtn { cursor: pointer; }
-</style>
\ No newline at end of file
+</style>

--
Gitblit v1.9.1