From 088c9cace064c64dc1848955a92dcaf5a32a13ad Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 11:50:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?:bug:=20=E5=AF=BC=E5=85=A5=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=97=B6=E6=98=BE=E7=A4=BA=E5=90=8E=E7=AB=AF=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E6=8F=90=E7=A4=BA=EF=BC=8C=E4=B8=8D=E5=86=8D=E8=AF=AF?= =?UTF-8?q?=E6=8A=A5=E7=B3=BB=E7=BB=9F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题(用户实测反馈):导入只有一个表头的空文件时,前端提示「系统错误」, 用户误以为系统崩溃;后端其实已返回明确文案(400 + 「导入数据不能为空」)。 根因(两处叠加): 1) axios 错误分支 reject(new Error(error)) 把原始错误包装掉, error.response 丢失、message 退化成 [object Object]; 2) 导入接口统一用 responseType:'blob',非 2xx 的响应体也是 Blob, 而 axios 错误分支不做解析,拿不到后端 msg。 修复: - src/axios.js:错误分支直接 reject 原始 axios 错误,保留 response; - src/utils/import-excel.js:新增 resolveErrorMessage,从错误响应的 Blob 中 解析后端 JSON 取 msg;解析不到时兜底为中文提示(不再暴露 「Request failed with status code xxx」这类英文技术文案)。 验证:vite build 通过;模拟真实响应体验证 400 + 空表场景, 提示由「系统错误」变为后端原文「未读取到数据行,请确认表头下方已填写内容」。 --- src/axios.js | 5 ++++- src/utils/import-excel.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/axios.js b/src/axios.js index cd29fa6..9064c7d 100644 --- a/src/axios.js +++ b/src/axios.js @@ -223,7 +223,10 @@ axios.interceptors.response.use( }, error => { NProgress.done(); - return Promise.reject(new Error(error)); + // 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response + // 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成 + // "[object Object]",用户只能看到无意义的通用提示。 + return Promise.reject(error); } ); diff --git a/src/utils/import-excel.js b/src/utils/import-excel.js index 425d5df..2e5126e 100644 --- a/src/utils/import-excel.js +++ b/src/utils/import-excel.js @@ -28,6 +28,34 @@ const parseBlobJson = async blob => { return text ? JSON.parse(text) : {}; }; +/** + * 解析导入失败时的错误提示文案。 + * + * 背景:导入接口统一用 `responseType: 'blob'` 请求,因此**非 2xx 响应体也是 Blob**。 + * axios 的错误分支不做解析,直接 reject,`error.message` 只有 + * 「Request failed with status code 400」这类通用文案(或全局兜底的「系统错误」), + * 用户会误以为系统崩了——而后端其实已返回了明确的中文业务提示(如「导入数据不能为空」)。 + * + * 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。 + */ +const resolveErrorMessage = async error => { + const blob = error?.response?.data ?? error?.data; + if (blob instanceof Blob) { + try { + const result = await parseBlobJson(blob); + if (result?.msg) return result.msg; + } catch (ignored) { + // 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案 + } + } + const fallback = error?.response?.data?.msg || error?.message; + // axios 的英文通用文案对用户没有意义,统一换成中文提示 + if (!fallback || /Request failed with status code/i.test(fallback)) { + return '导入失败,请检查导入文件后重试'; + } + return fallback; +}; + /** * 加工后端返回的导入失败明细 Excel。 * 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流, @@ -125,7 +153,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc option.onSuccess(res); } } catch (error) { - vm.$message.error(error.message || '导入失败'); + vm.$message.error(await resolveErrorMessage(error)); if (typeof option.onError === 'function') { option.onError(error); } From fc7df95c1aab9cfac9ee295ce667ad91858f1e36 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 12:21:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=BC=B9=E5=87=BA=E5=B9=B6=E4=B8=A2=E5=A4=B1=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - axios 拦截器:blob 请求(导入/导出)在业务校验失败时后端返回的是 JSON(Content-Type: application/json),而 axios 在 responseType='blob' 下把响应体解析成 Blob,直接读 res.data.msg 取不到任何字段, status 退回 res.status、文案落到「系统错误」兜底,用户误以为系统崩溃。 现按 Content-Type 判断:JSON 才解析回对象,真·文件流保持原样不动。 - 统一提示出口 notifiedError():弹窗后给 Error 打 _notified 标记, 业务层据此避免同一条错误被弹两次。 - import-excel:拦截器已提示过的错误不再重复 $message.error; >500/网络异常等仍由 resolveErrorMessage 从 Blob 中取 msg 兜底。 --- src/axios.js | 63 ++++++++++++++++++++++++++++++++------- src/utils/import-excel.js | 6 +++- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/axios.js b/src/axios.js index 9064c7d..3c1fab2 100644 --- a/src/axios.js +++ b/src/axios.js @@ -70,11 +70,44 @@ const redirectToLogin = () => { store.dispatch('FedLogOut').then(() => router.push({ path: '/login' })); }; +/** + * 统一弹出错误提示并返回带标记的 Error。 + * `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」, + * 避免同一条错误被弹两次。 + */ +const notifiedError = message => { + ElMessage({ + message: message, + type: 'error', + }); + const error = new Error(message); + error._notified = true; + return error; +}; + axios.defaults.timeout = 10000; //返回其他状态码 axios.defaults.validateStatus = function (status) { return status >= 200 && status <= 500; // 默认的 }; + +/** + * 判断响应是否为 JSON。 + * 用于 blob 请求(导入/导出)的兜底:后端在业务校验失败时返回的是 JSON + * (Content-Type: application/json),而不是文件流,需要解析后才能取到 msg。 + */ +const isJsonContentType = res => { + const headers = res.headers || {}; + const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase(); + return contentType.includes('application/json') || contentType.includes('text/json'); +}; + +/** + * 判断响应体是否已是解析好的对象(无需再解析)。 + * 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。 + */ +const isPlainObject = value => + value !== null && typeof value === 'object' && !(value instanceof Blob); //跨域请求,允许保存cookie axios.defaults.withCredentials = true; // NProgress Configuration @@ -140,7 +173,7 @@ axios.interceptors.request.use( //http response拦截 axios.interceptors.response.use( - res => { + async res => { NProgress.done(); //获取配置信息 const config = res.config; @@ -149,6 +182,22 @@ axios.interceptors.response.use( if (cryptoData) { res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey)); } + // blob 请求(文件下载、导入)的响应体是 Blob,直接读 res.data.code / res.data.msg + // 拿不到任何字段:状态码会退回 res.status、提示文案会落到「系统错误」兜底, + // 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。 + // 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。 + // 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。 + if (isJsonContentType(res) && !isPlainObject(res.data)) { + try { + const text = res.data instanceof Blob ? await res.data.text() : res.data; + const parsed = typeof text === 'string' && text ? JSON.parse(text) : null; + if (parsed && typeof parsed === 'object') { + res.data = parsed; + } + } catch (ignored) { + // 声明是 JSON 却解析失败:保持原样,交给下游按文件处理 + } + } //获取状态信息 const status = res.data.error_code || res.data.code || res.status; const statusWhiteList = website.statusWhiteList || []; @@ -204,20 +253,12 @@ axios.interceptors.response.use( // OAuth2 业务错误码提示 if (status > 2000 && !validatenull(res.data.error_description)) { - ElMessage({ - message: message, - type: 'error', - }); - return Promise.reject(new Error(message)); + return Promise.reject(notifiedError(message)); } // 其余非 200 响应统一提示 if (status !== 200) { - ElMessage({ - message: message, - type: 'error', - }); - return Promise.reject(new Error(message)); + return Promise.reject(notifiedError(message)); } return res; }, diff --git a/src/utils/import-excel.js b/src/utils/import-excel.js index 2e5126e..2ad6d55 100644 --- a/src/utils/import-excel.js +++ b/src/utils/import-excel.js @@ -153,7 +153,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc option.onSuccess(res); } } catch (error) { - vm.$message.error(await resolveErrorMessage(error)); + // 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次, + // 只保留一次清晰的中文提示。 + if (!error?._notified) { + vm.$message.error(await resolveErrorMessage(error)); + } if (typeof option.onError === 'function') { option.onError(error); }