✨ 优化主数据地图回填与表单校验间距,并修复运单导入筛选与组织树定位
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+99
-22
@@ -6,7 +6,31 @@ import 'nprogress/nprogress.css';
|
||||
|
||||
const excelPattern = /\.(xls|xlsx)$/;
|
||||
|
||||
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
|
||||
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 解析成的业务对象 */
|
||||
const isBusinessJson = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
!isBlobLike(value) &&
|
||||
!(value instanceof ArrayBuffer) &&
|
||||
('code' in value || 'success' in value || 'msg' in value);
|
||||
|
||||
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();
|
||||
@@ -23,8 +47,34 @@ const isExcelResponse = res => {
|
||||
);
|
||||
};
|
||||
|
||||
const parseBlobJson = async blob => {
|
||||
const text = await blob.text();
|
||||
/**
|
||||
* 解析导入接口响应体为 JSON 对象。
|
||||
* <p>
|
||||
* axios 拦截器在 Content-Type 为 JSON 时,会把 `responseType: 'blob'` 的 Blob
|
||||
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||
* `blob.text()` 时出现「blob.text is not a function」。
|
||||
*/
|
||||
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) : {};
|
||||
};
|
||||
|
||||
@@ -39,18 +89,21 @@ const parseBlobJson = async blob => {
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
const blob = error?.response?.data ?? error?.data;
|
||||
if (blob instanceof Blob) {
|
||||
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(blob);
|
||||
const result = await parseBlobJson(payload);
|
||||
if (result?.msg) return result.msg;
|
||||
} catch (ignored) {
|
||||
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
||||
}
|
||||
}
|
||||
const fallback = error?.response?.data?.msg || error?.message;
|
||||
const fallback = payload?.msg || error?.message;
|
||||
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
||||
if (!fallback || /Request failed with status code/i.test(fallback)) {
|
||||
if (!fallback || /Request failed with status code|\.text is not a function/i.test(fallback)) {
|
||||
return '导入失败,请检查导入文件后重试';
|
||||
}
|
||||
return fallback;
|
||||
@@ -68,8 +121,11 @@ const resolveErrorMessage = async error => {
|
||||
*/
|
||||
const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||
if (typeof failDetailDecorator !== 'function') return blob;
|
||||
if (!isBlobLike(blob) && !(blob instanceof ArrayBuffer)) {
|
||||
return blob;
|
||||
}
|
||||
const ExcelJS = await import('exceljs');
|
||||
const buf = await blob.arrayBuffer();
|
||||
const buf = typeof blob.arrayBuffer === 'function' ? await blob.arrayBuffer() : blob;
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(buf);
|
||||
await failDetailDecorator(workbook, { businessName });
|
||||
@@ -77,6 +133,31 @@ const decorateFailExcel = async (blob, businessName, failDetailDecorator) => {
|
||||
return new Blob([out], { type: 'application/vnd.ms-excel' });
|
||||
};
|
||||
|
||||
/** 通知上传组件成功:只传业务响应体,避免其误调 axios 响应上的 .text() */
|
||||
const notifyUploadSuccess = (option, res) => {
|
||||
if (typeof option?.onSuccess !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body = isBusinessJson(res?.data) ? res.data : { code: 200, success: true };
|
||||
option.onSuccess(body);
|
||||
} catch (callbackError) {
|
||||
console.warn('upload onSuccess callback failed', callbackError);
|
||||
}
|
||||
};
|
||||
|
||||
/** 通知上传组件失败 */
|
||||
const notifyUploadError = (option, error) => {
|
||||
if (typeof option?.onError !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
option.onError(error instanceof Error ? error : new Error(String(error?.message || error || '导入失败')));
|
||||
} catch (callbackError) {
|
||||
console.warn('upload onError callback failed', callbackError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开导入弹窗
|
||||
* @param {Object} vm 组件实例
|
||||
@@ -108,9 +189,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
const fileName = file && file.name ? file.name.toLowerCase() : '';
|
||||
if (!excelPattern.test(fileName)) {
|
||||
vm.$message.error('请上传 .xls,.xlsx 标准格式文件');
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||
}
|
||||
notifyUploadError(option, new Error('请上传 .xls,.xlsx 标准格式文件'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,12 +209,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
);
|
||||
vm.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = await parseBlobJson(res.data);
|
||||
if (result.code !== 200) {
|
||||
// 成功体常被 axios 拦截器提前解析成对象;优先走业务 JSON,避免再调 Blob.text
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result.code) !== 200) {
|
||||
vm.$message.error(result.msg || '导入失败');
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(new Error(result.msg || '导入失败'));
|
||||
}
|
||||
notifyUploadError(option, new Error(result.msg || '导入失败'));
|
||||
return;
|
||||
}
|
||||
vm.$message.success('导入完成');
|
||||
@@ -149,18 +227,17 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
} else if (typeof vm.initTree === 'function') {
|
||||
vm.initTree();
|
||||
}
|
||||
if (typeof option.onSuccess === 'function') {
|
||||
option.onSuccess(res);
|
||||
}
|
||||
// 上传组件 onSuccess 约定接收「响应体」,不是完整 axios 响应。
|
||||
// 若传入 axios 响应对象,部分环境下会按 Fetch Response 调用 .text(),
|
||||
// 从而在导入已成功后仍弹出「e.text is not a function」。
|
||||
notifyUploadSuccess(option, res);
|
||||
} catch (error) {
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
}
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(error);
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
loading.close();
|
||||
NProgress.done();
|
||||
|
||||
Reference in New Issue
Block a user