feat: 所有前端 API 请求加超时保护,超时/网络异常优雅提示,不再无限 pending
| | |
| | | import axios from 'axios' |
| | | import { Message } from 'element-ui' |
| | | |
| | | const api = axios.create({ |
| | | baseURL: '/api', |
| | | timeout: 30000 |
| | | timeout: 15000 |
| | | }) |
| | | |
| | | api.interceptors.request.use(config => { |
| | |
| | | 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) |
| | | } |
| | |
| | | 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()) }) |
| | |
| | | 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()) }) |
| | |
| | | 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) { |
| | |
| | | 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 |
| | |
| | | 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 } }) |
| | |
| | | .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() { |
| | |
| | | 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 + '」为已审核,报表导出不再提示未审核') |
| | |
| | | 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() { |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | 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 = '' }, |
| | |
| | | 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) }, |
| | |
| | | }) |
| | | .catch(e => { |
| | | this.dirImporting = false |
| | | if (e && e._toast) return |
| | | this.$message.error((e.response && e.response.data && e.response.data.message) || '目录不存在或无法访问:' + dir) |
| | | }) |
| | | }, |
| | |
| | | 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 }) |
| | |
| | | }) |
| | | this.overview = map |
| | | }) |
| | | .catch(() => {}) |
| | | .catch(e => { if (!e || !e._toast) this.$message.error('导入记录加载失败,请刷新重试') }) |
| | | .finally(() => { this.batchLoading = false }) |
| | | }, |
| | | batchKeyToType(k) { |
| | |
| | | this.page = 1 |
| | | this.loadData() |
| | | } |
| | | }).catch(() => { |
| | | }).catch(e => { |
| | | if (e && e._toast) return |
| | | this.$message.error('数据查询失败') |
| | | }).finally(() => { |
| | | this.loading = false |
| | |
| | | this.page = 1 |
| | | this.loadData() |
| | | } |
| | | }).catch(() => { |
| | | }).catch(e => { |
| | | if (e && e._toast) return |
| | | this.$message.error('解释查询失败') |
| | | }).finally(() => { |
| | | this.loading = false |
| | |
| | | } |
| | | }, |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | 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('网络错误,登录失败') |
| | | }) |
| | | }) |
| | |
| | | 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 => { |
| | |
| | | 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 => { |
| | |
| | | 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) |
| | |
| | | .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 |
| | |
| | | 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() } |
| | |
| | | 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) { |
| | |
| | | this.loadList() |
| | | }) |
| | | .catch(err => { |
| | | if (err && err._toast) return |
| | | const d = err && err.response && err.response.data |
| | | this.$message.error((d && d.message) || '保存失败') |
| | | }) |