feat: 所有前端 API 请求加超时保护,超时/网络异常优雅提示,不再无限 pending
11个文件已修改
80 ■■■■■ 已修改文件
traffic-audit-web/src/api/index.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/components/AiChatDialog.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/components/Captcha3D.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/AuditResult.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataView.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ExplainReview.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/Login.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/RuleManage.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/UserManage.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/api/index.js
@@ -1,8 +1,9 @@
import 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)
  }
traffic-audit-web/src/components/AiChatDialog.vue
@@ -113,7 +113,7 @@
      api.post('/llm/chat', {
        messages: [{ role: 'user', content: '请用结构化 Markdown(小节标题、加粗、列表)分析该企业的解释是否合理,先给结论,再列数据依据,最后给出处置建议。' }],
        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()) })
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
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('企业解释已保存')
          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 = '' },
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) {
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
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('解释查询失败')
      }).finally(() => {
        this.loading = false
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('网络错误,登录失败')
        })
      })
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
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() }
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) || '保存失败')
          })