diff --git a/src/axios.js b/src/axios.js index a3de4a3..455eb3d 100644 --- a/src/axios.js +++ b/src/axios.js @@ -105,12 +105,27 @@ const isJsonContentType = res => { return contentType.includes('application/json') || contentType.includes('text/json'); }; +/** + * 是否为可按文本读取的 Blob / Blob-like(含跨 realm)。 + * 禁止对普通业务 JSON 对象调用 .text()。 + */ +const isBlobLikeData = value => + value != null && + typeof value === 'object' && + typeof value.size === 'number' && + typeof value.type === 'string' && + typeof value.text === 'function'; + /** * 判断响应体是否已是解析好的对象(无需再解析)。 * 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。 */ const isPlainObject = value => - value !== null && typeof value === 'object' && !(value instanceof Blob); + value !== null && + typeof value === 'object' && + !(value instanceof Blob) && + !isBlobLikeData(value) && + !(value instanceof ArrayBuffer); //跨域请求,允许保存cookie axios.defaults.withCredentials = true; // NProgress Configuration @@ -190,15 +205,20 @@ axios.interceptors.response.use( // 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。 // 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。 // 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。 + // 仅对真正的 Blob/Blob-like 调 .text(),避免业务 JSON 已解析后出现 + // 「e.data.text is not a function」。 if (isJsonContentType(res) && !isPlainObject(res.data)) { try { let text = ''; - if (typeof res.data?.text === 'function') { - text = await res.data.text(); - } else if (typeof res.data === 'string') { - text = res.data; - } else if (typeof res.data?.arrayBuffer === 'function') { - text = new TextDecoder().decode(await res.data.arrayBuffer()); + const raw = res.data; + if (isBlobLikeData(raw) || (typeof Blob !== 'undefined' && raw instanceof Blob)) { + text = await raw.text(); + } else if (typeof raw === 'string') { + text = raw; + } else if (raw && typeof raw.arrayBuffer === 'function') { + text = new TextDecoder().decode(await raw.arrayBuffer()); + } else if (raw instanceof ArrayBuffer) { + text = new TextDecoder().decode(raw); } const parsed = typeof text === 'string' && text ? JSON.parse(text) : null; if (parsed && typeof parsed === 'object') { @@ -208,10 +228,11 @@ axios.interceptors.response.use( // 声明是 JSON 却解析失败:保持原样,交给下游按文件处理 } } - //获取状态信息 - const status = res.data.error_code || res.data.code || res.status; + // 获取状态信息:文件流没有 code/msg,必须退回 HTTP status,禁止读 Blob 上的幽灵字段 + const payload = isPlainObject(res.data) ? res.data : {}; + const status = Number(payload.error_code || payload.code || res.status); const statusWhiteList = website.statusWhiteList || []; - const message = res.data.msg || res.data.error_description || '系统错误'; + const message = payload.msg || payload.error_description || '系统错误'; //如果在白名单里则自行catch逻辑处理 if (statusWhiteList.includes(status)) return Promise.reject(res); diff --git a/src/utils/import-excel.js b/src/utils/import-excel.js index b6a9093..1bd5e02 100644 --- a/src/utils/import-excel.js +++ b/src/utils/import-excel.js @@ -88,7 +88,7 @@ export const parseBlobJson = async data => { * * 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。 */ -const resolveErrorMessage = async error => { +export const resolveImportErrorMessage = async error => { const payload = error?.response?.data ?? error?.data; if (isBusinessJson(payload) && payload.msg) { return payload.msg; @@ -109,6 +109,26 @@ const resolveErrorMessage = async error => { 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 流, @@ -235,7 +255,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc // 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次, // 只保留一次清晰的中文提示。 if (!error?._notified) { - vm.$message.error(await resolveErrorMessage(error)); + vm.$message.error(await resolveImportErrorMessage(error)); } notifyUploadError(option, error); } finally { diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index eb23da3..36aa8b5 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -13824,13 +13824,13 @@ export default { } const unavailableWaybills = this.selectionList.filter( row => - this.statusValue(row) !== 'pending' || + !['pending', 'processing', 'running'].includes(this.statusValue(row)) || row.loadingId || row.loadingManageId || row.loadingNo ); if (unavailableWaybills.length) { - this.$message.warning('公路配载仅支持进行中且未配载的运单'); + this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单'); return; } try { diff --git a/src/views/business/components/master-order-editor.vue b/src/views/business/components/master-order-editor.vue index 74ce647..6ad4fde 100644 --- a/src/views/business/components/master-order-editor.vue +++ b/src/views/business/components/master-order-editor.vue @@ -94,7 +94,7 @@
@@ -111,7 +111,7 @@
@@ -714,7 +714,7 @@ export default { 'msg', 'zip', ], - transports: ['公路运输', '铁路运输', '水路运输', '航空运输'], + transports: ['铁路运输', '水路运输', '航空运输', '公路运输'], quantityUnitOptions: [], quantityUnitLoading: false, form: { @@ -798,6 +798,7 @@ export default { methods: { formatContractOptionLabel, defaultQuantityUnit() { + if (this.quantityUnitOptions.includes('吨')) return '吨'; return this.quantityUnitOptions[0] || '吨'; }, loadQuantityUnitOptions() { @@ -1164,22 +1165,30 @@ export default { const field = this.addressModel(target); if (!field) return false; const siteCode = String(field.model[field.siteCode] || '').trim(); - // 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称, - // 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。 + // 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。 return Boolean(siteCode && siteCode !== '/'); }, + hasAddressValue(target) { + const field = this.addressModel(target); + if (!field) return false; + return Boolean( + String(field.model[field.address] || '').trim() || String(field.model[field.name] || '').trim() + ); + }, + tipEmptyAddressDetail(target) { + // 替代原「请先选择运输方式」placeholder:空地址点击时 toast,已有值不再提示。 + if (!this.hasAddressValue(target)) { + this.$message.warning('请输入详细地址'); + } + }, resolveAddressRegionCode(row = {}) { return row.regionCode || row.districtCode || row.areaCode || row.adcode || ''; }, openCommonAddress(target) { if (this.isBasicRouteReadonly) return; + this.tipEmptyAddressDetail(target); this.addressTarget = target; - if (this.addressTransportMode(target) !== 'road') { - this.stationQuery = {}; - this.stationPage.current = 1; - this.stationDialogVisible = true; - return; - } + // 暂不按运输方式限制站点:统一走常用地址选择。 this.addressQuery = {}; this.addressPage.current = 1; this.addressDialogVisible = true; @@ -1279,18 +1288,11 @@ export default { }, handleAddressDetailClick(target) { if (this.isBasicRouteReadonly) return; - if (this.addressTransportMode(target) !== 'road') { - this.openCommonAddress(target); - return; - } + this.tipEmptyAddressDetail(target); this.openAddressMap(target); }, openAddressMap(target) { if (this.isBasicRouteReadonly) return; - if (this.addressTransportMode(target) !== 'road') { - this.$message.info('非公路运输地址不能使用地图选择'); - return; - } const field = this.addressModel(target); if (!field) return; this.mapTarget = target; @@ -1690,7 +1692,11 @@ export default { '', cargoTypePath, quantity: row.quantity || '', - quantityUnit: row.quantityUnit || this.defaultQuantityUnit(), + // 常用货物单位若为空或「个」,新建总单统一默认「吨」 + quantityUnit: + !row.quantityUnit || String(row.quantityUnit).trim() === '个' + ? this.defaultQuantityUnit() + : row.quantityUnit, packageType: row.packageType || row.package || '', brand: row.brand || '', specification: row.specification || row.spec || '', @@ -1775,17 +1781,21 @@ export default { return formatUpdateUserName(row); }, selectRoute(route) { + // 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。 + this.form.finalTransportType = '公路运输'; + if (!this.form.routes.length) this.addRoute(); + if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输'; this.form.departureName = route.departureName || ''; this.form.departureAddress = route.departureAddress || ''; this.form.departureContact = route.departureContact || ''; this.form.departurePhone = route.departurePhone || ''; + this.form.departureSiteCode = ''; this.form.arrivalName = route.arrivalName || ''; this.form.arrivalAddress = route.arrivalAddress || ''; this.form.arrivalContact = route.arrivalContact || ''; this.form.arrivalPhone = route.arrivalPhone || ''; + this.form.arrivalSiteCode = ''; this.syncAddressRegionPaths(); - this.form.finalTransportType = '公路运输'; - if (!this.form.routes.length) this.addRoute(); this.routeDialogVisible = false; }, createRoute(index = this.form.routes.length) { diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index e7b489f..a701aaf 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -10414,13 +10414,13 @@ export default { } const unavailableWaybills = this.selectionList.filter( row => - !['processing', 'running'].includes(this.statusValue(row)) || + !['pending', 'processing', 'running'].includes(this.statusValue(row)) || row.loadingId || row.loadingManageId || row.loadingNo ); if (unavailableWaybills.length) { - this.$message.warning('公路配载仅支持进行中且未配载的运单'); + this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单'); return; } try { diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index ce1374d..9196fe0 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -635,10 +635,11 @@ min-width="100" />
+
+
可配载条件
+
+ 同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单。 +
+
配载后状态说明
+
+ 子运单:按项目过程配置是否需司机接单,无需接单则为进行中,需接单则为待执行。配载单:按所选运单所属项目的过程配置,需接单则为待执行,无需接单则为进行中。 +
+