From f45500301471948e474a0add1379fce2e3c6ce61 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 10 Oct 2026 16:20:26 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E4=BC=98=E5=8C=96=E4=B8=BB?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=9C=B0=E5=9B=BE=E5=9B=9E=E5=A1=AB=E4=B8=8E?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=A0=A1=E9=AA=8C=E9=97=B4=E8=B7=9D=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=E8=BF=90=E5=8D=95=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E4=B8=8E=E7=BB=84=E7=BB=87=E6=A0=91=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/axios.js | 9 +- src/components/address-map-picker/main.vue | 35 +- src/components/cron-editor/main.vue | 2 +- src/styles/element-ui.scss | 23 +- src/utils/import-excel.js | 121 ++++-- src/views/base/airport-master.vue | 347 ++++++++++++++++-- src/views/base/port-terminal.vue | 112 ++++-- src/views/base/railway-station.vue | 326 ++++++++++++++-- src/views/base/region.vue | 2 +- .../components/business-crud-page.vue | 6 +- .../components/master-order-dispatch.vue | 2 +- .../components/master-order-editor.vue | 2 +- .../components/shipping-template-page.vue | 4 +- .../components/transport-plan-page.vue | 4 +- .../components/waybill-import-dialog.vue | 27 +- .../components/waybill-manage-page.vue | 2 +- src/views/business/contract-manage-change.vue | 2 +- src/views/business/contract-manage.vue | 2 +- src/views/business/loading-manage.vue | 2 +- src/views/business/project-apply.vue | 2 +- src/views/business/transport-plan-import.vue | 2 +- src/views/business/voucher-manage.vue | 2 +- src/views/payment/bill-ledger-form.vue | 2 +- src/views/payment/bill-payment-form.vue | 2 +- src/views/payment/invoice-receipt-form.vue | 2 +- .../payment/payment-application-form.vue | 2 +- .../components/formal-settlement-editor.vue | 2 +- .../components/pre-settlement-editor.vue | 2 +- .../settlement-adjustment-editor.vue | 2 +- .../transport-reconciliation-editor.vue | 2 +- .../settlement/receivable-payable-detail.vue | 2 +- src/views/system/dept.vue | 146 +++++++- src/views/system/user.vue | 2 +- src/views/transportCapacity/driver.vue | 2 +- src/views/transportCapacity/ship.vue | 2 +- src/views/transportCapacity/vehicle.vue | 2 +- .../vehicle/credit-score-quantification.vue | 4 +- src/views/vehicle/customer-archive.vue | 6 +- 38 files changed, 1036 insertions(+), 182 deletions(-) diff --git a/src/axios.js b/src/axios.js index 3c1fab2..529a3f5 100644 --- a/src/axios.js +++ b/src/axios.js @@ -189,7 +189,14 @@ axios.interceptors.response.use( // 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。 if (isJsonContentType(res) && !isPlainObject(res.data)) { try { - const text = res.data instanceof Blob ? await res.data.text() : res.data; + 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 parsed = typeof text === 'string' && text ? JSON.parse(text) : null; if (parsed && typeof parsed === 'object') { res.data = parsed; diff --git a/src/components/address-map-picker/main.vue b/src/components/address-map-picker/main.vue index 3f2aa90..e4f4055 100644 --- a/src/components/address-map-picker/main.vue +++ b/src/components/address-map-picker/main.vue @@ -16,7 +16,11 @@
{{ status }}
@@ -322,8 +326,31 @@ export default { this.marker = null; } }, + syncSelectedFromMarker() { + // 确认时以当前标记点坐标为准,避免换地址后仍回填上一次经纬度 + if (!this.marker || typeof this.marker.getPosition !== 'function') { + return; + } + const point = this.marker.getPosition(); + if (!point) { + return; + } + const longitude = + typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude; + const latitude = + typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude; + if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) { + return; + } + this.selected = { + ...this.selected, + longitude: Number(longitude), + latitude: Number(latitude), + }; + }, confirm() { - if (!this.selected.longitude || !this.selected.latitude) { + this.syncSelectedFromMarker(); + if (!this.hasCoordinateValue(this.selected.longitude) || !this.hasCoordinateValue(this.selected.latitude)) { this.$message.warning('请先搜索或点击地图完成选点'); return; } @@ -333,8 +360,8 @@ export default { } this.$emit('confirm', { address: this.selected.address, - longitude: this.selected.longitude, - latitude: this.selected.latitude, + longitude: Number(this.selected.longitude), + latitude: Number(this.selected.latitude), country: this.selected.country || this.selected.countryName || '', province: this.selected.province || this.selected.provinceName || '', city: this.selected.city || this.selected.cityName || '', diff --git a/src/components/cron-editor/main.vue b/src/components/cron-editor/main.vue index a6963c8..3c79cb5 100644 --- a/src/components/cron-editor/main.vue +++ b/src/components/cron-editor/main.vue @@ -576,7 +576,7 @@ export default { } .mode-content :deep(.el-form-item) { - margin-bottom: 14px; + margin-bottom: 24px; } .mode-content :deep(.el-form-item:last-child) { diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss index 61bfe89..1440718 100644 --- a/src/styles/element-ui.scss +++ b/src/styles/element-ui.scss @@ -173,7 +173,10 @@ } .el-dialog .el-col { + // 取消列自身下边距,改由 form-item 统一控制行距; + // flow-root 避免子级 margin 折叠穿透导致换行后行距失效、校验提示被下一行遮挡 margin-bottom: 0; + display: flow-root; } .el-dialog .el-form-item, @@ -181,7 +184,7 @@ .el-dialog .el-form-item--small.el-form-item, .el-dialog .el-form-item--large.el-form-item, .el-dialog .el-form-item--medium.el-form-item { - //margin-bottom: 20px; + margin-bottom: 24px; } /* 全站弹窗:底部操作栏浮动效果(参照 business/project-apply) @@ -362,9 +365,14 @@ } } -// 4) 弹窗内表单项默认上下间距 16px(覆盖默认 18px / 22px,与 customer-archive 对齐) +// 4) 弹窗内表单项上下间距:预留绝对定位校验提示(约 14~18px)的可视空间 .el-dialog .el-form-item { - //margin-bottom: 16px; + margin-bottom: 24px; +} + +// 校验提示浮在控件下方,提高层级避免被下一行输入框背景遮挡 +.el-form-item__error { + z-index: 2; } // 5) 通用分组白卡组件():标题融入卡头 @@ -446,7 +454,7 @@ .dialog-form--carded { .el-form-item { align-items: center; - margin-bottom: 14px; + margin-bottom: 24px; } .el-form-item__label { @@ -1073,7 +1081,12 @@ td.el-table-fixed-column--right { align-items: normal; } .el-form-item--default { - margin-bottom: 14px; + margin-bottom: 24px; +} + +// 独立表单页(非弹窗)同样避免 el-col 折叠吃掉行距 +.el-form .el-col { + display: flow-root; } // 配套:星号必须保持「高度 auto + 继承行高」,stretch 才会按首行排布; diff --git a/src/utils/import-excel.js b/src/utils/import-excel.js index 2ad6d55..4d69c81 100644 --- a/src/utils/import-excel.js +++ b/src/utils/import-excel.js @@ -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 对象。 + *

+ * 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(); diff --git a/src/views/base/airport-master.vue b/src/views/base/airport-master.vue index 8ef4edd..3c94260 100644 --- a/src/views/base/airport-master.vue +++ b/src/views/base/airport-master.vue @@ -144,7 +144,7 @@