From 41487b9a8ad1353c21dd1ac2bfafcf8d1d4f34e4 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期五, 28 八月 2026 17:09:47 +0800
Subject: [PATCH] feat: 所有前端 API 请求加超时保护,超时/网络异常优雅提示,不再无限 pending

---
 traffic-audit-web/src/views/ReportExport.vue      |   11 +++--
 traffic-audit-web/src/views/RuleManage.vue        |    6 +-
 traffic-audit-web/src/views/DataImport.vue        |    6 ++-
 traffic-audit-web/src/views/Login.vue             |    7 ++-
 traffic-audit-web/src/components/Captcha3D.vue    |    4 +-
 traffic-audit-web/src/views/DataView.vue          |    3 +
 traffic-audit-web/src/views/UserManage.vue        |    3 +
 traffic-audit-web/src/views/AuditResult.vue       |   22 +++++-----
 traffic-audit-web/src/components/AiChatDialog.vue |    4 +-
 traffic-audit-web/src/api/index.js                |   11 +++++
 traffic-audit-web/src/views/ExplainReview.vue     |    3 +
 11 files changed, 48 insertions(+), 32 deletions(-)

diff --git a/traffic-audit-web/src/api/index.js b/traffic-audit-web/src/api/index.js
index b50c451..bc8a6c6 100644
--- a/traffic-audit-web/src/api/index.js
+++ b/traffic-audit-web/src/api/index.js
@@ -1,8 +1,9 @@
 锘縤mport axios from 'axios'
+import { Message } from 'element-ui'
 
 const api = axios.create({
   baseURL: '/api',
-  timeout: 30000
+  timeout: 15000
 })
 
 api.interceptors.request.use(config => {
@@ -17,6 +18,14 @@
     if (error.response && error.response.status === 401) {
       localStorage.removeItem('token')
       window.location.href = '/login'
+      return Promise.reject(error)
+    }
+    if (error.code === 'ECONNABORTED') {
+      error._toast = true
+      Message.error('璇锋眰瓒呮椂锛氭湇鍔″櫒绻佸繖鎴栨棤鍝嶅簲锛岃绋嶅悗閲嶈瘯')
+    } else if (!error.response) {
+      error._toast = true
+      Message.error('缃戠粶寮傚父锛氭棤娉曡繛鎺ユ湇鍔″櫒锛岃妫�鏌ョ綉缁滃悗閲嶈瘯')
     }
     return Promise.reject(error)
   }
diff --git a/traffic-audit-web/src/components/AiChatDialog.vue b/traffic-audit-web/src/components/AiChatDialog.vue
index efee353..82afaad 100644
--- a/traffic-audit-web/src/components/AiChatDialog.vue
+++ b/traffic-audit-web/src/components/AiChatDialog.vue
@@ -113,7 +113,7 @@
       api.post('/llm/chat', {
         messages: [{ role: 'user', content: '璇风敤缁撴瀯鍖� Markdown锛堝皬鑺傛爣棰樸�佸姞绮椼�佸垪琛級鍒嗘瀽璇ヤ紒涓氱殑瑙i噴鏄惁鍚堢悊锛屽厛缁欑粨璁猴紝鍐嶅垪鏁版嵁渚濇嵁锛屾渶鍚庣粰鍑哄缃缓璁��' }],
         context: this.context
-      })
+      }, { timeout: 120000 })
         .then(res => { this.pushAssistant(res.data || 'AI 鏈繑鍥炲唴瀹癸紝璇烽噸璇�') })
         .catch(() => { this.pushAssistant('AI 鍒嗘瀽澶辫触锛岃绋嶅悗閲嶈瘯鎴栦汉宸ュ垽鏂�') })
         .finally(() => { this.$nextTick(() => this.scrollToBottom()) })
@@ -147,7 +147,7 @@
       this.userScrolledUp = false
       this.chatLoading = true
       this.$nextTick(() => this.scrollToBottom())
-      api.post('/llm/chat', { messages: this.chatMessages, context: this.context })
+      api.post('/llm/chat', { messages: this.chatMessages, context: this.context }, { timeout: 120000 })
         .then(res => { this.pushAssistant(res.data || 'AI 鏈繑鍥炲唴瀹癸紝璇烽噸璇�') })
         .catch(() => { this.pushAssistant('AI 鍒嗘瀽澶辫触锛岃绋嶅悗閲嶈瘯鎴栦汉宸ュ垽鏂�') })
         .finally(() => { this.$nextTick(() => this.scrollToBottom()) })
diff --git a/traffic-audit-web/src/components/Captcha3D.vue b/traffic-audit-web/src/components/Captcha3D.vue
index 0264cc7..d710b33 100644
--- a/traffic-audit-web/src/components/Captcha3D.vue
+++ b/traffic-audit-web/src/components/Captcha3D.vue
@@ -119,7 +119,7 @@
       this.selectedIds = []
       this.errorMsg = ''
       this.$emit('reset')
-      axios.get('/api/auth/captcha')
+      axios.get('/api/auth/captcha', { timeout: 10000 })
         .then(res => {
           this.loading = false
           if (res.data.code === 200) {
@@ -176,7 +176,7 @@
       axios.post('/api/auth/captcha/verify', {
         captchaId: this.challenge.captchaId,
         captchaClickIds: this.selectedIds.slice()
-      })
+      }, { timeout: 10000 })
         .then(res => {
           this.loading = false
           const d = res.data
diff --git a/traffic-audit-web/src/views/AuditResult.vue b/traffic-audit-web/src/views/AuditResult.vue
index 7ef313c..9f19f80 100644
--- a/traffic-audit-web/src/views/AuditResult.vue
+++ b/traffic-audit-web/src/views/AuditResult.vue
@@ -336,7 +336,7 @@
       const fd = new FormData()
       fd.append('period', this.period)
       fd.append('reportType', this.reportType)
-      api.post('/audit/execute', fd)
+      api.post('/audit/execute', fd, { timeout: 120000 })
         .then(res => {
           const count = (res.data || []).length
           return api.get('/audit/ruleStats', { params: { period: this.period, reportType: this.reportType } })
@@ -361,7 +361,7 @@
         .then(() => api.get('/audit/results', { params: { period: this.period, reportType: this.reportType } }))
         .then(res => { this.applyResults(res.data) })
         .then(() => this.loadRuleStats())
-        .catch(() => { this.$message.error('瀹℃牳鎵ц澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error('瀹℃牳鎵ц澶辫触') })
         .finally(() => { this.loading = false })
     },
     markAudited() {
@@ -370,7 +370,7 @@
       const fd = new FormData()
       fd.append('period', this.period)
       fd.append('reportType', this.reportType)
-      api.post('/audit/markAudited', fd)
+      api.post('/audit/markAudited', fd, { timeout: 60000 })
         .then(res => {
           if (res.data) {
             this.$message.success('宸叉爣璁拌鏈熴��' + this.reportType + '銆嶄负宸插鏍革紝鎶ヨ〃瀵煎嚭涓嶅啀鎻愮ず鏈鏍�')
@@ -380,7 +380,7 @@
           return api.get('/audit/results', { params: { period: this.period, reportType: this.reportType } })
         })
         .then(res => { this.applyResults(res.data) })
-        .catch(() => { this.$message.error('鏍囪澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error('鏍囪澶辫触') })
         .finally(() => { this.marking = false })
     },
     queryResults() {
@@ -389,7 +389,7 @@
       api.get('/audit/results', { params: { period: this.period, reportType: this.reportType } })
         .then(res => { this.applyResults(res.data) })
         .then(() => this.loadRuleStats())
-        .catch(() => { this.$message.error('鏌ヨ澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error('鏌ヨ澶辫触') })
         .finally(() => { this.loading = false })
     },
     toggleExplain(reportId) {
@@ -412,13 +412,13 @@
       fd.append('reportType', this.reportType)
       fd.append('reportId', reportId)
       fd.append('explanation', explanation)
-      api.post('/explain/save', fd)
+      api.post('/explain/save', fd, { timeout: 60000 })
         .then(() => {
           this.$message.success('浼佷笟瑙i噴宸蹭繚瀛�')
           this.$set(this.explainDrafts, reportId, explanation)
           this.results.forEach(r => { if (r.reportId === reportId) r.verifyExplanation = explanation })
         })
-        .catch(e => { this.$message.error((e.response && e.response.data && e.response.data.message) || '淇濆瓨澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error((e.response && e.response.data && e.response.data.message) || '淇濆瓨澶辫触') })
         .finally(() => { this.savingExplainId = null })
     },
     resetDraft(reportId) {
@@ -450,9 +450,9 @@
       fd.append('id', row.id)
       fd.append('status', status)
       if (comment) fd.append('comment', comment)
-      api.post('/audit/review', fd)
+      api.post('/audit/review', fd, { timeout: 60000 })
         .then(() => { row.status = status; this.$message.success(status === 'CONFIRMED' ? '宸茬‘璁�' : '宸插拷鐣�') })
-        .catch(() => { this.$message.error('鎿嶄綔澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error('鎿嶄綔澶辫触') })
     },
     updateBatchSelection() { this.batchSelection = this.filteredResults.filter(r => r._selected) },
     toggleSelectAll(value) {
@@ -478,9 +478,9 @@
         fd.append('id', item.id)
         fd.append('status', status)
         if (comment) fd.append('comment', comment)
-        api.post('/audit/review', fd)
+        api.post('/audit/review', fd, { timeout: 60000 })
           .then(() => { item.status = status; done++; if (done === list.length) this.$message.success('宸�' + (status === 'CONFIRMED' ? '纭' : '蹇界暐') + ' ' + done + ' 鏉�') })
-          .catch(() => { this.$message.error('閮ㄥ垎鎿嶄綔澶辫触') })
+          .catch(e => { if (e && e._toast) return; this.$message.error('閮ㄥ垎鎿嶄綔澶辫触') })
       })
     },
     clearFilters() { this.enterpriseFilter = ''; this.ruleFilter = '' },
diff --git a/traffic-audit-web/src/views/DataImport.vue b/traffic-audit-web/src/views/DataImport.vue
index 7216ff3..dd789fa 100644
--- a/traffic-audit-web/src/views/DataImport.vue
+++ b/traffic-audit-web/src/views/DataImport.vue
@@ -437,7 +437,7 @@
           this.dirFiles = d.files || []
           this.dirFilesTruncated = !!d.filesTruncated
         })
-        .catch(e => { this.$message.error((e.response && e.response.data && e.response.data.message) || '鐩綍鍔犺浇澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error((e.response && e.response.data && e.response.data.message) || '鐩綍鍔犺浇澶辫触') })
         .finally(() => { this.dirLoading = false })
     },
     goDirParent() { if (this.dirParent) this.loadDirs(this.dirParent) },
@@ -505,6 +505,7 @@
         })
         .catch(e => {
           this.dirImporting = false
+          if (e && e._toast) return
           this.$message.error((e.response && e.response.data && e.response.data.message) || '鐩綍涓嶅瓨鍦ㄦ垨鏃犳硶璁块棶锛�' + dir)
         })
     },
@@ -585,6 +586,7 @@
           this.afterImport()
         })
         .catch(e => {
+          if (e && e._toast) return
           this.$message.error((e.response && e.response.data && e.response.data.message) || '鎵归噺瀵煎叆澶辫触')
         })
         .finally(() => { this.cityDirLoading = false })
@@ -676,7 +678,7 @@
           })
           this.overview = map
         })
-        .catch(() => {})
+        .catch(e => { if (!e || !e._toast) this.$message.error('瀵煎叆璁板綍鍔犺浇澶辫触锛岃鍒锋柊閲嶈瘯') })
         .finally(() => { this.batchLoading = false })
     },
     batchKeyToType(k) {
diff --git a/traffic-audit-web/src/views/DataView.vue b/traffic-audit-web/src/views/DataView.vue
index ace9b12..6084401 100644
--- a/traffic-audit-web/src/views/DataView.vue
+++ b/traffic-audit-web/src/views/DataView.vue
@@ -102,7 +102,8 @@
           this.page = 1
           this.loadData()
         }
-      }).catch(() => {
+      }).catch(e => {
+        if (e && e._toast) return
         this.$message.error('鏁版嵁鏌ヨ澶辫触')
       }).finally(() => {
         this.loading = false
diff --git a/traffic-audit-web/src/views/ExplainReview.vue b/traffic-audit-web/src/views/ExplainReview.vue
index 246cef6..c714ed7 100644
--- a/traffic-audit-web/src/views/ExplainReview.vue
+++ b/traffic-audit-web/src/views/ExplainReview.vue
@@ -166,7 +166,8 @@
           this.page = 1
           this.loadData()
         }
-      }).catch(() => {
+      }).catch(e => {
+        if (e && e._toast) return
         this.$message.error('瑙i噴鏌ヨ澶辫触')
       }).finally(() => {
         this.loading = false
diff --git a/traffic-audit-web/src/views/Login.vue b/traffic-audit-web/src/views/Login.vue
index 2227171..f7dc1a7 100644
--- a/traffic-audit-web/src/views/Login.vue
+++ b/traffic-audit-web/src/views/Login.vue
@@ -44,7 +44,7 @@
     }
   },
   created() {
-    axios.get('/api/auth/captcha-config')
+    axios.get('/api/auth/captcha-config', { timeout: 10000 })
       .then(res => {
         const d = res.data
         if (d && d.code === 200) this.captchaRequired = d.data.required !== false
@@ -75,7 +75,7 @@
           password: this.form.password,
           captchaId: this.captchaId,
           captchaClickIds: this.clickIds
-        }).then(res => {
+        }, { timeout: 15000 }).then(res => {
           this.loggingIn = false
           const d = res.data
           if (d.code === 200) {
@@ -95,8 +95,9 @@
             this.$message.error(d.message || '鐧诲綍澶辫触锛岃閲嶈瘯')
             if (this.$refs.captcha) this.$refs.captcha.load()
           }
-        }).catch(() => {
+        }).catch(e => {
           this.loggingIn = false
+          if (e && e.code === 'ECONNABORTED') { this.$message.error('鐧诲綍璇锋眰瓒呮椂锛岃閲嶈瘯'); return }
           this.$message.error('缃戠粶閿欒锛岀櫥褰曞け璐�')
         })
       })
diff --git a/traffic-audit-web/src/views/ReportExport.vue b/traffic-audit-web/src/views/ReportExport.vue
index ca276fb..c7935b4 100644
--- a/traffic-audit-web/src/views/ReportExport.vue
+++ b/traffic-audit-web/src/views/ReportExport.vue
@@ -304,7 +304,7 @@
       const m = this.curModule
       if (!m || !this.period) return
       const params = Object.assign(this.buildParams(), { types: m.items.map(i => i.key).join(',') })
-      api.get('/report/ready', { params })
+      api.get('/report/ready', { params, timeout: 60000 })
         .then(res => {
           const list = (res && res.data && res.data.items) || []
           list.forEach(r => {
@@ -349,7 +349,7 @@
       if (!items.length) { this.$message.warning('璇烽�夋嫨瑕侀瑙堢殑鎶ヨ〃'); return }
       this.loading = 'preview-' + m.key
       const params = Object.assign(this.buildParams(), { types: items.map(i => i.key).join(',') })
-      api.get('/report/preview', { params })
+      api.get('/report/preview', { params, timeout: 60000 })
         .then(res => {
           const list = (res && res.data && res.data.items) || []
           this.previewItems = list.map(it => {
@@ -387,18 +387,18 @@
       const params = this.buildParams()
       const doExport = () => {
         if (items.length === 1) {
-          api.get('/report/export/' + items[0].key, { params, responseType: 'blob' })
+          api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
             .then(res => { this.downloadBlob(res, items[0].fileName); this.$message.success('鎶ヨ〃瀵煎嚭鎴愬姛'); this.loading = '' })
             .catch(err => this.failWith(err))
           return
         }
         const zipParams = Object.assign({}, params, { types: items.map(i => i.key).join(',') })
-        api.get('/report/export/selected', { params: zipParams, responseType: 'blob' })
+        api.get('/report/export/selected', { params: zipParams, responseType: 'blob', timeout: 120000 })
           .then(res => { this.downloadBlob(res, '鐢熸垚_鎵�閫夋姤琛╛' + this.period + '.zip'); this.$message.success('宸插鍑� ' + items.length + ' 寮犳姤琛紙zip锛�'); this.loading = '' })
           .catch(err => this.failWith(err))
       }
       const readyParams = Object.assign({}, params, { types: items.map(i => i.key).join(',') })
-      api.get('/report/ready', { params: readyParams })
+      api.get('/report/ready', { params: readyParams, timeout: 60000 })
         .then(rd => {
           const list = (rd && rd.data && rd.data.items) || []
           const missing = list.filter(i => !i.ready)
@@ -420,6 +420,7 @@
         .catch(() => doExport())
     },
     failWith(err) {
+      if (err && err._toast) { this.loading = ''; return }
       let msg = ''
       const d = err && err.response && err.response.data
       if (d && typeof d === 'object' && d.message) msg = d.message
diff --git a/traffic-audit-web/src/views/RuleManage.vue b/traffic-audit-web/src/views/RuleManage.vue
index 30d2f57..e39ff03 100644
--- a/traffic-audit-web/src/views/RuleManage.vue
+++ b/traffic-audit-web/src/views/RuleManage.vue
@@ -91,19 +91,19 @@
       this.loading = true
       api.get('/rules/list', { params: { reportType: this.reportType } })
         .then(res => { this.rules = res.data || [] })
-        .catch(() => { this.$message.error('瑙勫垯鍔犺浇澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error('瑙勫垯鍔犺浇澶辫触') })
         .finally(() => { this.loading = false })
     },
     toggleRule(row) {
       api.post('/rules/toggle', null, { params: { id: row.id, enabled: row.isEnabled } })
         .then(() => { this.$message.success((row.isEnabled ? '宸插惎鐢細' : '宸插仠鐢細') + row.ruleCode) })
-        .catch(() => { row.isEnabled = row.isEnabled ? 0 : 1; this.$message.error('鎿嶄綔澶辫触') })
+        .catch(e => { if (e && e._toast) return; row.isEnabled = row.isEnabled ? 0 : 1; this.$message.error('鎿嶄綔澶辫触') })
     },
     saveThreshold(row) {
       api.post('/rules/update', {
         id: row.id, threshold: row.threshold, alertLevel: row.alertLevel
       }).then(() => { this.$message.success('宸蹭繚瀛橈細' + row.ruleCode) })
-        .catch(() => { this.$message.error('淇濆瓨澶辫触') })
+        .catch(e => { if (e && e._toast) return; this.$message.error('淇濆瓨澶辫触') })
     }
   },
   mounted() { this.loadRules() }
diff --git a/traffic-audit-web/src/views/UserManage.vue b/traffic-audit-web/src/views/UserManage.vue
index abae3fe..6f9cc03 100644
--- a/traffic-audit-web/src/views/UserManage.vue
+++ b/traffic-audit-web/src/views/UserManage.vue
@@ -124,7 +124,7 @@
       this.loading = true
       api.get('/user/list', { params: { keyword: this.keyword } })
         .then(res => { this.list = res.data || [] })
-        .catch(() => this.$message.error('鐢ㄦ埛鍒楄〃鍔犺浇澶辫触'))
+        .catch(e => { if (e && e._toast) return; this.$message.error('鐢ㄦ埛鍒楄〃鍔犺浇澶辫触') })
         .finally(() => { this.loading = false })
     },
     moduleKeys(modules) {
@@ -164,6 +164,7 @@
             this.loadList()
           })
           .catch(err => {
+            if (err && err._toast) return
             const d = err && err.response && err.response.data
             this.$message.error((d && d.message) || '淇濆瓨澶辫触')
           })

--
Gitblit v1.9.1