import { importBlob } from '@/api/common'; import { downloadXls } from '@/utils/util'; import { ElLoading } from 'element-plus'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; const excelPattern = /\.(xls|xlsx)$/; /** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */ export const isBlobLike = value => value != null && typeof value === 'object' && typeof value.size === 'number' && typeof value.type === 'string' && (typeof value.text === 'function' || typeof value.arrayBuffer === 'function'); /** axios 拦截器已把 JSON Blob 解析成的业务对象 */ export const isBusinessJson = value => value != null && typeof value === 'object' && !isBlobLike(value) && !(value instanceof ArrayBuffer) && ('code' in value || 'success' in value || 'msg' in value); export const isExcelResponse = res => { // 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理 if (isBusinessJson(res?.data)) { return false; } // 只有文件流才可能是失败明细;普通对象没有 .text/.arrayBuffer if (!isBlobLike(res?.data)) { return false; } const headers = res.headers || {}; const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase(); const contentDisposition = String(headers['content-disposition'] || '').toLowerCase(); if ( contentType.includes('application/vnd.ms-excel') || contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') ) { return true; } // 网关或对象存储可能将 Excel 流标记为二进制类型,此时通过下载文件名识别。 return ( contentType.includes('application/octet-stream') && /\.(xls|xlsx)(?:["';]|$)/.test(contentDisposition) ); }; /** * 解析导入接口响应体为 JSON 对象。 *
* axios 拦截器在 Content-Type 为 JSON 时,会把 `responseType: 'blob'` 的 Blob
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
* `blob.text()` 时出现「blob.text is not a function」。
*/
export const parseBlobJson = async data => {
if (data == null) {
return {};
}
if (isBusinessJson(data)) {
return data;
}
if (typeof data === 'object' && !isBlobLike(data) && !(data instanceof ArrayBuffer)) {
return data;
}
if (typeof data === 'string') {
return data ? JSON.parse(data) : {};
}
let text = '';
if (typeof data.text === 'function') {
text = await data.text();
} else if (typeof data.arrayBuffer === 'function') {
text = new TextDecoder().decode(await data.arrayBuffer());
} else if (data instanceof ArrayBuffer) {
text = new TextDecoder().decode(data);
}
return text ? JSON.parse(text) : {};
};
/**
* 解析导入失败时的错误提示文案。
*
* 背景:导入接口统一用 `responseType: 'blob'` 请求,因此**非 2xx 响应体也是 Blob**。
* axios 的错误分支不做解析,直接 reject,`error.message` 只有
* 「Request failed with status code 400」这类通用文案(或全局兜底的「系统错误」),
* 用户会误以为系统崩了——而后端其实已返回了明确的中文业务提示(如「导入数据不能为空」)。
*
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
*/
export const resolveImportErrorMessage = async error => {
const payload = error?.response?.data ?? error?.data;
if (isBusinessJson(payload) && payload.msg) {
return payload.msg;
}
if (isBlobLike(payload) || typeof payload === 'string') {
try {
const result = await parseBlobJson(payload);
if (result?.msg) return result.msg;
} catch (ignored) {
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
}
}
const fallback = payload?.msg || error?.message;
// axios 的英文通用文案对用户没有意义,统一换成中文提示
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
return '导入失败,请检查导入文件后重试';
}
return fallback;
};
/**
* 统一消费导入接口响应:成功 JSON / 失败 Excel 流。
* 兼容 axios 已把 JSON Blob 解析成对象的情况,禁止再对普通对象调 .text()。
*/
export const consumeImportResponse = async (res, options = {}) => {
const { businessName = '导入', dayjsFormat } = options;
if (isExcelResponse(res)) {
return {
kind: 'excel',
blob: res?.data,
fileName: `${businessName}导入失败明细${dayjsFormat || ''}.xlsx`,
};
}
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
if (Number(result?.code) !== 200) {
throw new Error(result?.msg || '导入失败');
}
return { kind: 'success', result };
};
/**
* 加工后端返回的导入失败明细 Excel。
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
* 交由 decorator 完成改写原因列、调整列宽等加工,再导出新的 blob;
* 否则原样返回 blob(保持旧行为,不影响未使用 decorator 的模块)。
* @param {Blob} blob 后端返回的失败明细 Excel 流
* @param {String} businessName 业务名称(如「货物类型」)
* @param {Function} [failDetailDecorator] async (workbook, { businessName }) => void
* @returns {Promise