From ee7160e393b794c7dd5695d34699a4f28b5461aa Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 25 Sep 2026 17:45:21 +0800 Subject: [PATCH 01/16] =?UTF-8?q?:sparkles:=20=E5=AE=8C=E5=96=84=E5=AE=A2?= =?UTF-8?q?=E5=95=86MK=E5=AE=A1=E6=A0=B8=E6=8F=90=E4=BA=A4=E3=80=81?= =?UTF-8?q?=E5=9C=B0=E5=8C=BA=E6=A0=A1=E9=AA=8C=E4=B8=8E=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提交增加 loading,拆分所在地区/详细地址校验,支持 MK 审核跳转,并在已有流程实例时跳过重复提交。 Co-authored-by: Cursor --- src/api/system/business-process.js | 27 ++++++ src/utils/mk-approval.js | 13 ++- src/views/vehicle/customer-archive.vue | 127 +++++++++++++++++-------- 3 files changed, 124 insertions(+), 43 deletions(-) diff --git a/src/api/system/business-process.js b/src/api/system/business-process.js index 44f9526..0818de1 100644 --- a/src/api/system/business-process.js +++ b/src/api/system/business-process.js @@ -29,3 +29,30 @@ export const processDelete = data => { data, }); }; + +/** + * 查询 MK 流程实例是否仍存在(有数据则无需删除/重新提交) + * @param {Object} params + * @param {string} params.bizId 业务 id(formInstanceId) + */ +export const hasMkProcessInstance = params => { + return request({ + url: '/blade-system/businessProcess/hasMkProcessInstance', + method: 'get', + params, + }); +}; + +/** + * 获取 MK 审批页跳转链接(后端按 thirdParty.mk 配置拼接,含 SSO) + * @param {Object} params + * @param {string} [params.bizId] 业务 id(如客商 id) + * @param {string} [params.processInstanceId] 流程实例 id + */ +export const getMKApprovalUrl = params => { + return request({ + url: '/blade-system/businessProcess/getMKApprovalUrl', + method: 'get', + params, + }); +}; diff --git a/src/utils/mk-approval.js b/src/utils/mk-approval.js index 5b02da7..9bbb524 100644 --- a/src/utils/mk-approval.js +++ b/src/utils/mk-approval.js @@ -1,5 +1,5 @@ import { getDictionary } from '@/api/system/dictbiz'; -import { processDelete, processSubmit } from '@/api/system/business-process'; +import { hasMkProcessInstance, processDelete, processSubmit } from '@/api/system/business-process'; import { ElMessage } from 'element-plus'; export const MK_BIZ = { @@ -53,6 +53,12 @@ export function isMkEnabled(list = []) { return String(matched?.dictKey ?? '').trim() === '1'; } +/** 加载业务字典并判断是否开启 MK */ +export async function loadMkEnabled() { + const list = await loadMkTemplateList(); + return isMkEnabled(list); +} + export async function resolveMkTemplateCode(dictName, list) { const dictList = list || (await loadMkTemplateList()); const matched = dictList.find(item => String(item.dictValue || '').trim() === dictName); @@ -78,6 +84,11 @@ export async function submitMkApprovalFlow({ if (!isMkEnabled(list)) { return; } + // 先查 MK 流程实例:已有数据则不删除、不重新提交 + const existsRes = await hasMkProcessInstance({ bizId: String(formInstanceId) }); + if (existsRes?.data?.data) { + return; + } if ((conf.rejected || []).includes(approvalStatus)) { await processDelete({ formInstanceId: String(formInstanceId) }); } diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 31df353..cefecfc 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -155,9 +155,21 @@ > 复评 + + 审核 + 审核不通过 @@ -374,7 +390,7 @@ - +
{ - const regionName = (this.archiveForm.registeredRegionName || '').trim(); - const detailAddress = String(value || '').trim(); - if (!regionName) { - callback(new Error('请选择省市区')); - } else if (!detailAddress) { - callback(new Error('请输入详细地址')); - } else if (detailAddress.length > 255) { - callback(new Error('详细地址最多255个字符')); - } else { - callback(); - } - }; return { form: {}, query: {}, @@ -1749,6 +1753,8 @@ export default { approved: { label: '审核通过', type: 'success' }, rejected: { label: '审核不通过', type: 'danger' }, }, + /** 业务字典 mk_template「开启MK」=1 */ + mkEnabled: false, formRules: { customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }], fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }], @@ -1771,8 +1777,10 @@ export default { registeredCapital: [{ validator: validateNonNegative, trigger: 'blur' }], maxCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }], applyCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }], + registeredRegionName: [{ required: true, message: '请选择省市区', trigger: 'change' }], registeredDetailAddress: [ - { validator: validateRegisteredAddress, trigger: ['change', 'blur'] }, + { required: true, message: '请输入详细地址', trigger: 'blur' }, + { max: 255, message: '详细地址最多255个字符', trigger: 'blur' }, ], businessScope: [ { @@ -2028,6 +2036,7 @@ export default { this.initDeptTree(); this.initRegionOptions(); this.initBusinessDictionaries(); + this.initMkEnabled(); this.initScoreQuantificationOptions(); if (this.isArchivePage) { this.reviewMode = this.$route.query.review === '1'; @@ -2863,12 +2872,12 @@ export default { handleRegisteredRegionChange(value) { if (!value || !value.length) { this.archiveForm.registeredRegionName = ''; - this.$refs.archiveForm?.validateField('registeredDetailAddress'); + this.$refs.archiveForm?.validateField('registeredRegionName'); return; } this.$nextTick(() => { this.archiveForm.registeredRegionName = this.getRegisteredRegionLabels(value).join(''); - this.$refs.archiveForm?.validateField('registeredDetailAddress'); + this.$refs.archiveForm?.validateField('registeredRegionName'); }); }, handleInvoiceRegisteredRegionChange(value) { @@ -2899,6 +2908,15 @@ export default { this.sortQualificationFiles(); }); }, + initMkEnabled() { + loadMkEnabled() + .then(enabled => { + this.mkEnabled = !!enabled; + }) + .catch(() => { + this.mkEnabled = false; + }); + }, initScoreQuantificationOptions() { this.scoreQuantificationLoading = true; return getCreditScoreQuantificationList(1, 9999, { status: 1 }) @@ -4206,30 +4224,37 @@ export default { // 仅提交时落变更记录;保存不记录 archive.recordChange = true; if (!this.validateScoreForApproval(archive)) return; - submit(archive).then(res => { - const data = res.data.data; - const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id; - if (!id) { - this.closeArchive(); - this.onLoad(this.page); - this.$message({ type: 'success', message: '保存成功,请在列表提交审批' }); - return; - } - const fullName = - (data && typeof data === 'object' ? data.fullName : '') || - archive.fullName || - this.archiveForm.fullName || - ''; - this.submitCustomerApproval( - id, - fullName, - archive.approvalStatus || this.archiveForm.approvalStatus - ).then(() => { - this.closeArchive(); - this.onLoad(this.page); - this.$message({ type: 'success', message: '提交成功!' }); - }); + const loading = ElLoading.service({ + lock: true, + text: '提交中', + background: 'rgba(255, 255, 255, 0.7)', }); + submit(archive) + .then(res => { + const data = res.data.data; + const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id; + if (!id) { + this.closeArchive(); + this.onLoad(this.page); + this.$message({ type: 'success', message: '保存成功,请在列表提交审批' }); + return; + } + const fullName = + (data && typeof data === 'object' ? data.fullName : '') || + archive.fullName || + this.archiveForm.fullName || + ''; + return this.submitCustomerApproval( + id, + fullName, + archive.approvalStatus || this.archiveForm.approvalStatus + ).then(() => { + this.closeArchive(); + this.onLoad(this.page); + this.$message({ type: 'success', message: '提交成功!' }); + }); + }) + .finally(() => loading.close()); }); }, /** @@ -4928,6 +4953,24 @@ export default { this.$message({ type: 'success', message: '已撤回至草稿状态!' }); }); }, + handleMkAudit(row) { + if (!row?.id) { + this.$message.warning('客商ID为空,无法打开审核页'); + return; + } + getMKApprovalUrl({ bizId: String(row.id) }) + .then(res => { + const url = res?.data?.data; + if (!url) { + this.$message.error('获取审核跳转地址失败'); + return; + } + window.open(url, '_blank'); + }) + .catch(() => { + this.$message.error('获取审核跳转地址失败'); + }); + }, handleApprove(row) { this.$confirm('是否确认审核通过?', '提示', { confirmButtonText: '确定', From 079be78c2cd05185dfe3f2cd30c338c13a1f57e9 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 25 Sep 2026 17:45:21 +0800 Subject: [PATCH 02/16] =?UTF-8?q?:sparkles:=20=E4=BC=98=E5=8C=96=E9=85=8D?= =?UTF-8?q?=E8=BD=BD=E8=AF=A6=E6=83=85=E6=89=A7=E8=A1=8C=E6=89=93=E5=8D=A1?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E5=8D=95=E5=88=87=E6=8D=A2=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 执行详情按运单切换,补充打卡记录时间线与状态展示。 Co-authored-by: Cursor --- src/views/business/loading-manage.vue | 1180 ++++++++++++++++++++++--- 1 file changed, 1034 insertions(+), 146 deletions(-) diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index 459c6dc..7fc4461 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -609,31 +609,195 @@ show-overflow-tooltip />
- + + +
+ + +
+
+ {{ record.nodeName || record.nodeCode || '打卡' }} + {{ + record.statusName || (record.punched ? '已打卡' : '未打卡') + }} + 在途 + 异常 +
+ +
暂未打卡
+
+
+
+ +
+
+ +
+ + 返回文件夹 + + {{ voucherFolderName(voucherFolder) }} +
+
+
+ +
+ {{ voucherFolderName(image) }} +
+
+ +
+
+ +
+
+ +
+ {{ photo.label || '凭证' }} +
+
+
+
+
+
+ - + + + 常用 +
@@ -1374,8 +1570,11 @@
{{ tag }} + >
关闭 + +
+ + + 搜索 + +
+
+
+ +
+
+ {{ routeChangeMapStatus + }} 行政区划:{{ routeChangeMapSelected.regionName }} +
+ + + @@ -1488,13 +1743,18 @@ import { getList as getPlanList } from '@/api/business/transport-plan'; import { getList as getWaybillList, getDetail as getWaybillDetail, + getPunchRecords as getWaybillPunchRecords, } from '@/api/business/waybill-manage'; import { getList as getContractList } from '@/api/business/contract-manage'; import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type'; import { getList as getCustomerList } from '@/api/vehicle/customer-archive'; import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource'; -import { getList as getProcessConfigList } from '@/api/business/process-config'; +import { + getList as getProcessConfigList, + getDetail as getProcessConfigDetail, + getVoucherImages as getProcessConfigVoucherImages, +} from '@/api/business/process-config'; import { getList as getCommonAddressList } from '@/api/base/common-address'; import { carrierTypeOptions, @@ -1504,9 +1764,10 @@ import { } from '@/option/business/loading-manage'; import { option as waybillOption } from '@/option/business/waybill-manage'; import { ElMessage, ElMessageBox } from 'element-plus'; -import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue'; +import { Location, Rank, Refresh, Search, Setting } from '@element-plus/icons-vue'; import dayjs from 'dayjs'; import { isMobile } from '@/utils/validate'; +import { normalizeMapSearchResults } from '@/utils/map-search'; const defaultTransportType = ''; const defaultCandidateTransportType = ''; @@ -1598,6 +1859,21 @@ const normalizeLoadingMileage = value => { return value; }; +// 货物等业务字段用 -1 表示未填写,展示时留空 +const blankCargoSentinel = value => { + if (value === undefined || value === null || value === '') return ''; + if (Number(value) === -1) return ''; + return value; +}; + +const sanitizeCargoRow = (row = {}) => { + const result = { ...row }; + Object.keys(result).forEach(key => { + result[key] = blankCargoSentinel(result[key]); + }); + return result; +}; + // 提交时未填写里程传 null,禁止自动填充 -1 const toSubmitMileage = value => { const normalized = normalizeLoadingMileage(value); @@ -1611,6 +1887,14 @@ const createCopiedDialogForm = (detail = {}) => { Object.keys(form).forEach(prop => { if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop]; }); + let taskInfo = {}; + try { + const parsed = JSON.parse(form.taskInfoJson || '{}'); + taskInfo = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch (error) { + taskInfo = {}; + } + const { routeChangeRecords, ...copiedTaskInfo } = taskInfo; return { ...form, id: '', @@ -1620,7 +1904,7 @@ const createCopiedDialogForm = (detail = {}) => { dataSource: '手工创建', currentProcessNode: '接单', mileage: normalizeLoadingMileage(form.mileage), - taskInfoJson: '', + taskInfoJson: JSON.stringify(copiedTaskInfo), businessStatus: 'draft', }; }; @@ -1680,12 +1964,14 @@ const uniqueText = (rows, prop) => export default { components: { Rank, + Search, }, data() { return { Rank, Location, Refresh, + Search, Setting, loading: false, tableSize: 'default', @@ -1740,9 +2026,22 @@ export default { statusOptions: loadingStatusOptions, dataSourceOptions: loadingManageConfig.dataSourceOptions, config: loadingManageConfig, - processNodes: [], - processProjectId: '', - processProjectOptions: [], + processDetailTab: 'punch', + processWaybillId: '', + processWaybillOptions: [], + punchRecords: [], + punchRecordsLoading: false, + driverUploads: [], + hasRelatedVoucher: false, + voucherImages: [], + voucherFolders: [], + voucherFolder: null, + voucherFolderView: false, + voucherImagesLoading: false, + voucherImagesLoaded: false, + attachmentImagePreviewVisible: false, + attachmentImagePreviewUrls: [], + attachmentImagePreviewIndex: 0, routeChangeVisible: false, routeChangeTab: 'change', routeChangeRows: [], @@ -1750,12 +2049,24 @@ export default { routeChangeRemark: '', routeChangeRecords: [], routeChangeDragIndex: -1, + routeChangeOriginalAddresses: [], + routeChangeOrderDirty: false, commonAddressVisible: false, commonAddressLoading: false, commonAddressRows: [], commonAddressQuery: { addressName: '' }, commonAddressPage: { currentPage: 1, pageSize: 10, total: 0 }, commonAddressTarget: null, + routeChangeMapVisible: false, + routeChangeMapLoading: false, + routeChangeMapKeyword: '', + routeChangeMapStatus: '可搜索地址或点击地图选点', + routeChangeMapSelected: {}, + routeChangeMapSearchResults: [], + routeChangeMapTarget: null, + routeChangeMapInstance: null, + routeChangeMapMarker: null, + routeChangeMapGeocoder: null, detailAmap: null, }; }, @@ -1763,6 +2074,24 @@ export default { dialogReadonly() { return this.dialogMode === 'view'; }, + // 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后 + orderedPunchRecords() { + const records = Array.isArray(this.punchRecords) ? this.punchRecords : []; + return records + .map((record, index) => ({ + record, + index, + punchedAt: this.punchRecordTimestamp(record), + })) + .sort((a, b) => { + const aPunched = a.punchedAt !== null; + const bPunched = b.punchedAt !== null; + if (aPunched && bPunched) return a.punchedAt - b.punchedAt || a.index - b.index; + if (aPunched !== bPunched) return aPunched ? -1 : 1; + return a.index - b.index; + }) + .map(item => item.record); + }, taskCarrierPlaceholder() { if (!this.selectedWaybillRows.length) return '请先选择运单'; if (!this.taskCarrierLoading && !this.taskCarrierOptions.length) return '暂无可用承运商合同'; @@ -2457,6 +2786,7 @@ export default { if (status === 'draft') { return [ { type: 'edit', label: '编辑' }, + { type: 'copy', label: '复制' }, { type: 'delete', label: '删除' }, ]; } @@ -2468,24 +2798,36 @@ export default { if (row.driverRejected) { actions.push({ type: 'reassign', label: '重新派单' }); } - actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' }); + actions.push( + { type: 'edit', label: '编辑' }, + { type: 'copy', label: '复制' }, + { type: 'cancel', label: '取消' } + ); return actions; } if (status === 'running') { return [ { type: 'view', label: '详情' }, { type: 'complete', label: '完成' }, + { type: 'copy', label: '复制' }, ]; } if (status === 'completed' || status === 'cancelled') { - return [{ type: 'view', label: '详情' }]; + return [ + { type: 'view', label: '详情' }, + { type: 'copy', label: '复制' }, + ]; } - return [{ type: 'view', label: '详情' }]; + return [ + { type: 'view', label: '详情' }, + { type: 'copy', label: '复制' }, + ]; }, handleRowAction(type, row) { const actionMap = { view: () => this.gotoLoadingDetail(row), edit: () => this.openLoadingDialog('edit', row), + copy: () => this.gotoLoadingCopy(row), reassign: () => this.gotoLoadingReassign(row), delete: () => this.deleteLoading(row), start: () => this.startLoading(row), @@ -2507,6 +2849,18 @@ export default { query: { id, name: '配载单详情' }, }); }, + // 复制:进入新增页并带回原配载数据 + gotoLoadingCopy(row = {}) { + const id = row.id || row.loadingId || row.loadingManageId; + if (!id) { + ElMessage.warning('配载单不存在,无法复制'); + return; + } + this.$router.push({ + path: '/business/loading-manage/form', + query: { mode: 'add', copyId: id, name: '新增配载管理' }, + }); + }, // 重新派单改为独立页面 gotoLoadingReassign(row = {}) { if (!row.id) { @@ -2585,10 +2939,11 @@ export default { this.dialogForm.waybillIdsJson, this.dialogForm.loadingSubNos ); - this.syncProcessProjectOptions(); + this.syncProcessWaybillOptions(); this.restoreRouteAndCargo(); this.restoreRouteChangeRecords(); - await this.loadProcessNodes(); + this.loadPunchRecords(); + this.probeHasRelatedVoucher(); this.$nextTick(() => this.initDetailAmap()); await this.loadCandidateWaybills(); } else { @@ -2622,23 +2977,38 @@ export default { this.candidatePage.currentPage = 1; this.candidatePage.total = 0; this.candidateSearchVisible = false; - this.processNodes = []; - this.processProjectId = ''; - this.processProjectOptions = []; + this.processDetailTab = 'punch'; + this.processWaybillId = ''; + this.processWaybillOptions = []; + this.punchRecords = []; + this.driverUploads = []; + this.hasRelatedVoucher = false; + this.voucherImages = []; + this.voucherFolders = []; + this.voucherFolder = null; + this.voucherFolderView = false; + this.voucherImagesLoading = false; + this.voucherImagesLoaded = false; + this.attachmentImagePreviewVisible = false; + this.attachmentImagePreviewUrls = []; + this.attachmentImagePreviewIndex = 0; }, // 离开本页(标签缓存/卸载)时收起页内子弹窗,避免 append-to-body 的弹窗 DOM 残留在 body 上 closeInnerDialogs() { this.routeChangeVisible = false; this.commonAddressVisible = false; + this.routeChangeMapVisible = false; this.candidateSearchVisible = false; }, restoreRouteAndCargo() { const routeRows = parseJsonArray(this.dialogForm.routeJson); const cargoRows = parseJsonArray(this.dialogForm.goodsJson); this.routeNodes = routeRows.length - ? routeRows + ? this.syncRouteNodeLockedFlags(routeRows, this.selectedWaybillRows) : this.buildRouteNodes(this.selectedWaybillRows); - this.cargoRows = cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows); + this.cargoRows = (cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows)).map( + row => sanitizeCargoRow(row) + ); }, restoreRouteChangeRecords() { try { @@ -2652,70 +3022,177 @@ export default { this.routeChangeRecords = []; } }, - async loadProcessNodes() { - if (!this.processProjectId) { - this.processNodes = []; + syncProcessWaybillOptions() { + this.processWaybillOptions = (this.selectedWaybillRows || []) + .filter(row => row?.id) + .map(row => ({ + id: row.id, + waybillNo: row.waybillNo || String(row.id), + })); + const currentExists = this.processWaybillOptions.some( + item => String(item.id) === String(this.processWaybillId) + ); + this.processWaybillId = currentExists + ? this.processWaybillId + : this.processWaybillOptions[0]?.id || ''; + this.processDetailTab = 'punch'; + this.resetVoucherState(); + }, + resetVoucherState() { + this.hasRelatedVoucher = false; + this.voucherImages = []; + this.voucherFolders = []; + this.voucherFolder = null; + this.voucherFolderView = false; + this.voucherImagesLoading = false; + this.voucherImagesLoaded = false; + }, + handleProcessWaybillChange() { + this.processDetailTab = 'punch'; + this.resetVoucherState(); + this.loadPunchRecords(); + this.probeHasRelatedVoucher(); + }, + handleProcessDetailTabChange(tab) { + if (tab === 'punch' || tab === 'driverUpload') this.loadPunchRecords(); + if (tab === 'batchSupplement') this.loadVoucherImages(); + }, + probeHasRelatedVoucher() { + const waybillId = this.processWaybillId; + const waybill = (this.selectedWaybillRows || []).find( + row => String(row.id) === String(waybillId) + ); + const projectId = waybill?.projectId; + if (!waybillId || !projectId) { + this.hasRelatedVoucher = false; return; } - try { - const projectId = String(this.processProjectId); - const rows = unwrapRecords( - await getProcessConfigList(1, 100, { projectIds: projectId, status: 1 }) - ); - const config = - rows.find(item => { - const projectIds = String(item.projectIds || '') + getProcessConfigList(1, 100, { projectIds: projectId, status: 1 }) + .then(res => { + const config = unwrapRecords(res).find(row => { + const projectIds = String(row.projectIds || '') .split(',') - .map(value => value.trim()) - .filter(Boolean); - return projectIds.includes(projectId) && item.nodeConfigJson; - }) || {}; - this.processNodes = this.parseProcessConfigNodes(config); - } catch (error) { - this.processNodes = []; - } - }, - syncProcessProjectOptions() { - const projectMap = new Map(); - this.selectedWaybillRows.forEach(row => { - const id = row.projectId; - if (!id || projectMap.has(String(id))) return; - projectMap.set(String(id), { - id, - projectName: row.projectName || row.projectNames || String(id), + .map(item => item.trim()); + return projectIds.includes(String(projectId)); + }); + if (!config?.id) { + this.hasRelatedVoucher = false; + return null; + } + return getProcessConfigDetail(config.id, waybillId); + }) + .then(detailRes => { + if (!detailRes) return; + const detail = detailRes?.data?.data || detailRes?.data || {}; + this.hasRelatedVoucher = Boolean(detail.hasRelatedVoucher); + }) + .catch(() => { + this.hasRelatedVoucher = false; }); - }); - if (!projectMap.size && this.dialogForm.projectId) { - projectMap.set(String(this.dialogForm.projectId), { - id: this.dialogForm.projectId, - projectName: this.dialogForm.projectName || String(this.dialogForm.projectId), - }); - } - this.processProjectOptions = [...projectMap.values()]; - this.processProjectId = this.processProjectOptions[0]?.id || ''; }, - parseProcessConfigNodes(config = {}) { - let nodes = []; - try { - const parsed = - typeof config.nodeConfigJson === 'string' - ? JSON.parse(config.nodeConfigJson) - : config.nodeConfigJson; - nodes = Array.isArray(parsed) ? parsed : parsed?.nodes || []; - } catch (error) { - nodes = []; - } - const includedNodes = String(config.includedNodes || '') - .split(',') - .map(value => value.trim()) - .filter(Boolean); - return nodes.filter( - node => - node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name)) + punchRecordTimestamp(record = {}) { + if (record.punched === false || record.statusName === '未打卡') return null; + const raw = record.punchTime || record.punchAt || record.punchDateTime || ''; + const text = String(raw || '').trim(); + if (!text) return null; + const parsed = dayjs(text); + if (parsed.isValid()) return parsed.valueOf(); + const fallback = new Date(text.replace(/-/g, '/')).getTime(); + return Number.isNaN(fallback) ? null : fallback; + }, + loadPunchRecords() { + const waybillId = this.processWaybillId; + if (!waybillId || this.punchRecordsLoading) return; + this.punchRecordsLoading = true; + getWaybillPunchRecords(waybillId) + .then(res => { + const data = res?.data?.data || res?.data || {}; + this.punchRecords = Array.isArray(data.records) ? data.records : []; + this.driverUploads = Array.isArray(data.driverUploads) ? data.driverUploads : []; + if (data.hasRelatedVoucher) this.hasRelatedVoucher = true; + }) + .catch(() => { + this.punchRecords = []; + this.driverUploads = []; + }) + .finally(() => { + this.punchRecordsLoading = false; + }); + }, + previewDriverUploadPhoto(photos, index) { + const list = (photos || []).map(item => item?.url).filter(Boolean); + if (!list.length) return; + this.attachmentImagePreviewUrls = list; + this.attachmentImagePreviewIndex = Math.min( + Math.max(Number(index) || 0, 0), + list.length - 1 ); + this.attachmentImagePreviewVisible = true; }, - async handleProcessProjectChange() { - await this.loadProcessNodes(); + loadVoucherImages() { + if (!this.processWaybillId || this.voucherImagesLoading || this.voucherImagesLoaded) { + return; + } + this.voucherImagesLoading = true; + getProcessConfigVoucherImages(this.processWaybillId) + .then(res => { + this.voucherImages = unwrapRecords(res); + this.voucherFolders = this.voucherImages.filter(item => this.isVoucherFolder(item)); + this.voucherFolder = null; + this.voucherFolderView = false; + if (this.voucherImages.length) this.hasRelatedVoucher = true; + this.voucherImagesLoaded = true; + }) + .finally(() => { + this.voucherImagesLoading = false; + }); + }, + isVoucherFolder(item) { + return Boolean(item && (item.isFolder === true || item.type === 'folder')); + }, + voucherFolderName(folder) { + return folder?.folderName || folder?.name || folder?.plateNo || '凭证文件夹'; + }, + openVoucherFolder(folder) { + if (!this.processWaybillId || !this.isVoucherFolder(folder)) return; + if (!folder.voucherId || !folder.folderName) { + ElMessage.warning('凭证文件夹信息不完整'); + return; + } + this.voucherImagesLoading = true; + getProcessConfigVoucherImages(this.processWaybillId, { + voucherId: folder.voucherId, + folderName: folder.folderName, + }) + .then(res => { + this.voucherFolder = folder; + this.voucherFolderView = true; + this.voucherImages = unwrapRecords(res).filter(item => !this.isVoucherFolder(item)); + }) + .finally(() => { + this.voucherImagesLoading = false; + }); + }, + backToVoucherFolders() { + this.voucherFolder = null; + this.voucherFolderView = false; + this.voucherImages = this.voucherFolders; + }, + previewVoucherImage(index) { + const list = (this.voucherImages || []) + .filter(item => !this.isVoucherFolder(item)) + .map(item => item?.url) + .filter(Boolean); + if (!list.length) return; + const current = this.voucherImages[index]; + const currentUrl = current?.url; + const currentIndex = Math.max( + 0, + list.findIndex(url => url === currentUrl) + ); + this.attachmentImagePreviewUrls = list; + this.attachmentImagePreviewIndex = currentIndex >= 0 ? currentIndex : 0; + this.attachmentImagePreviewVisible = true; }, previewAttachment(file) { if (!file.url) { @@ -2760,9 +3237,6 @@ export default { handleTrackAction(action) { ElMessage.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据'); }, - handleProcessAction(action) { - ElMessage.info(action === 'supplement' ? '补录执行功能暂未接入' : '批量补录功能暂未接入'); - }, openRouteChangeDialog() { this.routeChangeTab = 'change'; this.routeChangeRemark = ''; @@ -2773,7 +3247,18 @@ export default { departureAddress: row.departureAddress || row.departureName || '', arrivalAddress: row.arrivalAddress || row.arrivalName || '', })); - this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows); + const builtNodes = this.buildRouteNodes(this.routeChangeRows); + this.routeChangeNodes = this.routeNodes.length + ? this.syncRouteNodeLockedFlags( + this.routeNodes.map(node => ({ + ...node, + tags: [...(node.tags || [])], + })), + this.routeChangeRows + ) + : builtNodes; + this.routeChangeOriginalAddresses = this.routeChangeNodes.map(node => node.address); + this.routeChangeOrderDirty = false; this.routeChangeVisible = true; }, routeChangeTypeText(index) { @@ -2787,26 +3272,255 @@ export default { return 'middle'; }, handleRouteChangeDragStart(index) { + if (this.isRouteChangeNodeLocked(this.routeChangeNodes[index])) { + this.routeChangeDragIndex = -1; + return; + } this.routeChangeDragIndex = index; }, handleRouteChangeDrop(index) { if (this.routeChangeDragIndex < 0 || this.routeChangeDragIndex === index) return; + if ( + this.isRouteChangeNodeLocked(this.routeChangeNodes[this.routeChangeDragIndex]) || + this.isRouteChangeNodeLocked(this.routeChangeNodes[index]) + ) { + this.routeChangeDragIndex = -1; + return; + } const nodes = [...this.routeChangeNodes]; const [node] = nodes.splice(this.routeChangeDragIndex, 1); nodes.splice(index, 0, node); + const originalLockedKeys = this.routeChangeNodes.map(item => + this.isRouteChangeNodeLocked(item) ? item.key : null + ); + const nextLockedKeys = nodes.map(item => + this.isRouteChangeNodeLocked(item) ? item.key : null + ); + if ( + originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex]) + ) { + ElMessage.warning('已完成运单对应的路线不可调整'); + this.routeChangeDragIndex = -1; + return; + } this.routeChangeNodes = nodes; + this.routeChangeOrderDirty = true; this.routeChangeDragIndex = -1; }, updateRouteChangeNodes() { - this.routeChangeNodes = this.buildRouteNodes(this.routeChangeRows); + const previousAddresses = this.routeChangeNodes.map(node => node.address); + const builtNodes = this.buildRouteNodes(this.routeChangeRows); + const remaining = [...builtNodes]; + const orderedNodes = []; + previousAddresses.forEach(address => { + const index = remaining.findIndex(node => node.address === address); + if (index >= 0) { + orderedNodes.push(remaining.splice(index, 1)[0]); + } + }); + this.routeChangeNodes = [...orderedNodes, ...remaining]; + }, + isRouteChangeOrderChanged() { + if (!this.routeChangeOrderDirty) return false; + const currentAddresses = this.routeChangeNodes.map(node => node.address); + if (currentAddresses.length !== this.routeChangeOriginalAddresses.length) return true; + return currentAddresses.some( + (address, index) => address !== this.routeChangeOriginalAddresses[index] + ); + }, + formatRouteChangeOrderText(addresses = []) { + return addresses.filter(Boolean).join(' → ') || '-'; + }, + isWaybillCompleted(row = {}) { + const status = String(row.businessStatus || row.status || row.waybillStatus || ''); + if (status === 'completed') return true; + const statusName = String(row.businessStatusName || row.statusName || ''); + return statusName.includes('已完成'); + }, + isRouteChangeNodeLocked(node = {}) { + return Boolean(node?.locked); + }, + syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) { + return (nodes || []).map(node => { + const locked = (node.tags || []).some(tag => { + const match = String(tag).match(/^(装|卸)(\d+)$/); + if (!match) return false; + const row = rows[Number(match[2]) - 1]; + return row ? this.isWaybillCompleted(row) : false; + }); + return { ...node, locked }; + }); }, openCommonAddressDialog(row, field) { + if (this.isWaybillCompleted(row)) return; this.commonAddressTarget = { row, field }; this.commonAddressQuery = { addressName: '' }; this.commonAddressPage.currentPage = 1; this.commonAddressVisible = true; this.loadCommonAddresses(); }, + openRouteChangeMapDialog(row, field) { + if (this.isWaybillCompleted(row)) return; + this.routeChangeMapTarget = { row, field }; + this.routeChangeMapKeyword = row[`${field}Address`] || row[`${field}Name`] || ''; + this.routeChangeMapSelected = { + longitude: row[`${field}Longitude`] || '', + latitude: row[`${field}Latitude`] || '', + detailAddress: this.routeChangeMapKeyword, + regionName: row[`${field}Name`] || '', + regionCode: row[`${field}RegionCode`] || '', + }; + this.routeChangeMapSearchResults = []; + this.routeChangeMapStatus = this.routeChangeMapSelected.longitude + ? '已加载当前选点,可重新选点' + : '可搜索地址或点击地图选点'; + this.routeChangeMapVisible = true; + }, + initRouteChangeMap() { + this.loadDetailAmap() + .then(() => + this.$nextTick(() => { + if (!this.$refs.routeChangeMap) return; + const center = this.routeChangeMapSelected.longitude + ? [ + Number(this.routeChangeMapSelected.longitude), + Number(this.routeChangeMapSelected.latitude), + ] + : [116.40769, 39.89945]; + if (!this.routeChangeMapInstance) { + this.routeChangeMapInstance = new window.AMap.Map(this.$refs.routeChangeMap, { + center, + zoom: this.routeChangeMapSelected.longitude ? 14 : 11, + }); + window.AMap.plugin(['AMap.Geocoder'], () => { + this.routeChangeMapGeocoder = new window.AMap.Geocoder(); + }); + this.routeChangeMapInstance.on('click', event => + this.pickRouteChangeMap(event.lnglat) + ); + } else { + this.routeChangeMapInstance.resize(); + } + if (this.routeChangeMapSelected.longitude) { + this.renderRouteChangeMapMarker(center); + } + }) + ) + .catch(() => ElMessage.error('高德地图组件加载失败')); + }, + destroyRouteChangeMap() { + this.routeChangeMapMarker?.setMap?.(null); + this.routeChangeMapMarker = null; + this.routeChangeMapInstance?.destroy?.(); + this.routeChangeMapInstance = null; + this.routeChangeMapGeocoder = null; + this.routeChangeMapSearchResults = []; + this.routeChangeMapTarget = null; + }, + ensureRouteChangeMapGeocoder() { + if (this.routeChangeMapGeocoder) return Promise.resolve(this.routeChangeMapGeocoder); + return new Promise(resolve => { + window.AMap.plugin(['AMap.Geocoder'], () => { + this.routeChangeMapGeocoder = new window.AMap.Geocoder(); + resolve(this.routeChangeMapGeocoder); + }); + }); + }, + searchRouteChangeMap() { + const keyword = String(this.routeChangeMapKeyword || '').trim(); + if (!keyword) { + ElMessage.warning('请输入地址关键词'); + return; + } + this.routeChangeMapLoading = true; + this.loadDetailAmap() + .then(() => this.ensureRouteChangeMapGeocoder()) + .then( + () => + new Promise((resolve, reject) => { + this.routeChangeMapGeocoder.getLocation(keyword, (status, result) => { + if (status === 'complete') resolve(result); + else reject(result); + }); + }) + ) + .then(result => { + this.routeChangeMapSearchResults = normalizeMapSearchResults(result, keyword); + const point = this.routeChangeMapSearchResults[0]?.location; + if (!point) { + this.routeChangeMapStatus = '未找到匹配地址'; + ElMessage.warning('地图搜索无匹配地址'); + return; + } + this.pickRouteChangeMap(point, keyword); + }) + .catch(() => { + this.routeChangeMapStatus = '地图搜索失败'; + ElMessage.error('地图搜索失败'); + }) + .finally(() => { + this.routeChangeMapLoading = false; + }); + }, + selectRouteChangeMapSearchResult(item) { + if (item?.location) { + this.pickRouteChangeMap(item.location, item.address || item.name || ''); + } + }, + pickRouteChangeMap(point, keyword) { + const longitude = point.getLng ? point.getLng() : point.lng; + const latitude = point.getLat ? point.getLat() : point.lat; + this.routeChangeMapSelected = { + longitude, + latitude, + detailAddress: keyword || this.routeChangeMapKeyword, + regionName: '', + regionCode: '', + }; + this.renderRouteChangeMapMarker([longitude, latitude]); + this.routeChangeMapStatus = '已选点,可确认回填'; + this.ensureRouteChangeMapGeocoder().then(() => { + this.routeChangeMapGeocoder.getAddress([longitude, latitude], (status, result) => { + if (status !== 'complete') return; + const component = result.regeocode?.addressComponent || {}; + this.routeChangeMapSelected = { + ...this.routeChangeMapSelected, + detailAddress: + result.regeocode?.formattedAddress || this.routeChangeMapSelected.detailAddress, + regionName: [component.province, component.city, component.district] + .filter(Boolean) + .join(''), + regionCode: component.adcode || '', + }; + this.routeChangeMapKeyword = this.routeChangeMapSelected.detailAddress; + this.routeChangeMapStatus = '已解析地址,可确认回填'; + }); + }); + }, + renderRouteChangeMapMarker(position) { + if (!this.routeChangeMapInstance) return; + this.routeChangeMapMarker?.setMap?.(null); + this.routeChangeMapMarker = new window.AMap.Marker({ position }); + this.routeChangeMapMarker.setMap(this.routeChangeMapInstance); + this.routeChangeMapInstance.setCenter(position); + }, + confirmRouteChangeMap() { + const target = this.routeChangeMapTarget; + if (!target || !this.routeChangeMapSelected.longitude) return; + const { row, field } = target; + row[`${field}Address`] = + this.routeChangeMapSelected.detailAddress || this.routeChangeMapKeyword || ''; + if (this.routeChangeMapSelected.regionName) { + row[`${field}Name`] = this.routeChangeMapSelected.regionName; + } + row[`${field}Longitude`] = this.routeChangeMapSelected.longitude; + row[`${field}Latitude`] = this.routeChangeMapSelected.latitude; + if (this.routeChangeMapSelected.regionCode) { + row[`${field}RegionCode`] = this.routeChangeMapSelected.regionCode; + } + this.updateRouteChangeNodes(); + this.routeChangeMapVisible = false; + }, async loadCommonAddresses() { this.commonAddressLoading = true; try { @@ -2841,19 +3555,34 @@ export default { row.departureAddress !== row.originalDepartureAddress || row.arrivalAddress !== row.originalArrivalAddress ); - if (!changeRows.length && !this.routeChangeRemark) { - ElMessage.warning('请修改地址或填写变更备注'); + const orderChanged = this.isRouteChangeOrderChanged(); + if (!changeRows.length && !orderChanged && !this.routeChangeRemark) { + ElMessage.warning('请修改地址、调整路线顺序或填写变更备注'); return; } - const content = - changeRows - .map( - row => - `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${ - row.departureAddress - };到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}` - ) - .join(';') || '调整运输路线顺序'; + const contentParts = []; + if (changeRows.length) { + contentParts.push( + changeRows + .map( + row => + `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress} → ${ + row.departureAddress + };到货地 ${row.originalArrivalAddress} → ${row.arrivalAddress}` + ) + .join(';') + ); + } + if (orderChanged) { + contentParts.push( + `路线顺序:${this.formatRouteChangeOrderText( + this.routeChangeOriginalAddresses + )} 调整为 ${this.formatRouteChangeOrderText( + this.routeChangeNodes.map(node => node.address) + )}` + ); + } + const content = contentParts.join(';') || '调整运输路线顺序'; this.routeChangeRecords.unshift({ changeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), changeUser: '当前用户', @@ -3039,18 +3768,34 @@ export default { }, buildRouteNodes(rows) { const nodeMap = new Map(); - const appendNode = (address, displayAddress, tag) => { + const appendNode = (address, displayAddress, tag, locked) => { if (!address) return; - if (!nodeMap.has(address)) nodeMap.set(address, { address, displayAddress, tags: [] }); - nodeMap.get(address).tags.push(tag); + if (!nodeMap.has(address)) { + nodeMap.set(address, { address, displayAddress, tags: [], locked: false }); + } + const node = nodeMap.get(address); + node.tags.push(tag); + if (locked) node.locked = true; }; rows.forEach((row, rowIndex) => { + const locked = this.isWaybillCompleted(row); const departure = row.departureAddress || row.departureName; - appendNode(departure, this.formatCandidateAddress(row, 'departure'), `装${rowIndex + 1}`); + appendNode( + departure, + this.formatCandidateAddress(row, 'departure'), + `装${rowIndex + 1}`, + locked + ); }); rows.forEach((row, rowIndex) => { + const locked = this.isWaybillCompleted(row); const arrival = row.arrivalAddress || row.arrivalName; - appendNode(arrival, this.formatCandidateAddress(row, 'arrival'), `卸${rowIndex + 1}`); + appendNode( + arrival, + this.formatCandidateAddress(row, 'arrival'), + `卸${rowIndex + 1}`, + locked + ); }); return [...nodeMap.values()].map((node, index) => ({ ...node, @@ -3068,13 +3813,37 @@ export default { return 'middle'; }, handleRouteDragStart(index) { + if (this.dialogReadonly || this.isRouteChangeNodeLocked(this.routeNodes[index])) { + this.routeDragIndex = -1; + return; + } this.routeDragIndex = index; }, handleRouteDrop(index) { - if (this.routeDragIndex < 0 || this.routeDragIndex === index) return; + if (this.dialogReadonly || this.routeDragIndex < 0 || this.routeDragIndex === index) return; + if ( + this.isRouteChangeNodeLocked(this.routeNodes[this.routeDragIndex]) || + this.isRouteChangeNodeLocked(this.routeNodes[index]) + ) { + this.routeDragIndex = -1; + return; + } const rows = [...this.routeNodes]; const [target] = rows.splice(this.routeDragIndex, 1); rows.splice(index, 0, target); + const originalLockedKeys = this.routeNodes.map(item => + this.isRouteChangeNodeLocked(item) ? item.key : null + ); + const nextLockedKeys = rows.map(item => + this.isRouteChangeNodeLocked(item) ? item.key : null + ); + if ( + originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex]) + ) { + ElMessage.warning('已完成运单对应的路线不可调整'); + this.routeDragIndex = -1; + return; + } this.routeDragIndex = -1; if (!this.validateRouteEndpoints(rows)) return; this.routeNodes = rows; @@ -3112,7 +3881,7 @@ export default { }); }, normalizeCargoRow(waybill, item = {}) { - return { + return sanitizeCargoRow({ projectName: item.projectName || waybill.projectName || '', cargoName: item.cargoName || item.goodsName || waybill.cargoName || '', cargoType: item.cargoType || item.goodsType || waybill.cargoType || '', @@ -3147,7 +3916,7 @@ export default { waybill.specificationModel || '', remark: item.remark || '', - }; + }); }, applyLoadingMileageFromDetail(form = this.dialogForm) { let taskMileage = ''; @@ -3302,6 +4071,8 @@ export default { }; this.applyLoadingMileageFromDetail(this.dialogForm); ElMessage.success('暂存成功'); + sessionStorage.setItem(loadingManageListRefreshKey, '1'); + this.closeLoadingDialog(); this.loadTable(); } finally { this.dialogSaving = ''; @@ -3944,6 +4715,12 @@ export default { border: 1px solid #eff1f7; background: #fff; cursor: move; + + &.is-locked { + cursor: not-allowed; + background: #f5f7fa; + color: #909399; + } } .loading-manage-dialog__route-handle { @@ -4193,7 +4970,74 @@ export default { gap: 8px; } .loading-detail__process-actions .el-select { - width: 180px; + width: 200px; + } + .loading-detail__punch-record { + display: flex; + flex-direction: column; + gap: 6px; + } + .loading-detail__punch-record-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + color: #303133; + } + .loading-detail__punch-record-line { + font-size: 13px; + color: #606266; + + span + span { + margin-left: 12px; + } + } + .loading-detail__punch-record-photos, + .loading-detail__driver-upload-grid, + .loading-detail__voucher-image-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + margin-top: 4px; + min-height: 80px; + } + .loading-detail__driver-upload-item, + .loading-detail__voucher-image-item { + display: flex; + flex-direction: column; + gap: 6px; + cursor: pointer; + + .el-image { + display: block; + width: 100%; + aspect-ratio: 1; + border: 1px solid #eff1f7; + border-radius: 4px; + overflow: hidden; + background: #f5f7fa; + } + } + .loading-detail__voucher-image-item--folder .el-image { + aspect-ratio: auto; + padding: 12px; + background: transparent; + border: none; + } + .loading-detail__driver-upload-label, + .loading-detail__voucher-folder-name { + font-size: 12px; + color: #606266; + line-height: 18px; + text-align: center; + word-break: break-all; + } + .loading-detail__voucher-folder-toolbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; } .loading-detail__map { width: 100%; @@ -4201,12 +5045,25 @@ export default { border: 1px solid #eff1f7; } } +:global(.loading-route-change-dialog .el-dialog__body) { + background: #fff; +} .loading-route-change-dialog { .loading-route-change-dialog__original { color: #a8abb2; line-height: 20px; margin-bottom: 4px; } + .loading-route-change-dialog__address-row { + display: flex; + align-items: center; + gap: 8px; + + .el-input { + flex: 1; + min-width: 0; + } + } .loading-route-change-dialog__location { cursor: pointer; color: #909399; @@ -4227,6 +5084,16 @@ export default { padding: 10px 12px; background: #f5f6f7; cursor: move; + + &.is-locked { + cursor: not-allowed; + background: #eef0f3; + color: #909399; + } + } + .loading-route-change-dialog__route-handle-placeholder { + width: 14px; + height: 14px; } .loading-route-change-dialog__route-type { width: 30px; @@ -4255,6 +5122,27 @@ export default { line-height: 22px; } } +.loading-route-change-map-dialog { + .loading-route-change-map-dialog__toolbar { + display: flex; + gap: 8px; + margin-bottom: 12px; + + .el-input { + flex: 1; + min-width: 0; + } + } + .loading-route-change-map-dialog__map { + width: 100%; + min-width: 0; + height: 440px; + } + .loading-route-change-map-dialog__status { + margin-top: 8px; + color: #606266; + } +} .loading-common-address-dialog__toolbar { display: flex; gap: 8px; From 088c9cace064c64dc1848955a92dcaf5a32a13ad Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Sat, 26 Sep 2026 11:50:49 +0800 Subject: [PATCH 03/16] =?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 04/16] =?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); } From 98fbf123f50b1a83be25efff7bcdecba440718f7 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 27 Sep 2026 21:26:55 +0800 Subject: [PATCH 05/16] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E8=BD=A6?= =?UTF-8?q?=E5=8A=A1=E9=9D=9E=E5=BF=85=E5=A1=AB-1=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E5=AE=8C=E5=96=84=E6=80=BB=E5=8D=95=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E4=B8=8E=E8=BF=90=E5=8D=95=E8=A1=A8=E5=8D=95=E4=BD=93?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调度页限制上一段到达后才可调度;司机候选项点击展开;车务多模块-1显示为空;同步配载/总单编辑与导入相关优化。 Co-authored-by: Cursor --- src/api/business/waybill-manage.js | 16 +- src/components/address-map-picker/main.vue | 65 +++-- src/option/business/contract-manage.js | 4 +- src/option/vehicle/accident-record.js | 7 +- .../vehicle/annual-inspection-record.js | 6 +- src/option/vehicle/insurance-record.js | 6 +- src/option/vehicle/mileage-record.js | 9 +- src/option/vehicle/oil-electric-record.js | 9 +- src/option/vehicle/tire-replacement-record.js | 9 +- src/option/vehicle/violation-record.js | 9 +- src/utils/map-search.js | 150 ++++++++++-- .../components/master-order-detail.vue | 19 +- .../components/master-order-dispatch.vue | 229 +++++++++++++++--- .../components/master-order-editor.vue | 158 ++++++++++-- .../components/waybill-import-dialog.vue | 208 +++++++++++----- .../components/waybill-manage-page.vue | 45 +++- src/views/business/contract-manage.vue | 57 ++++- src/views/business/loading-manage.vue | 10 +- src/views/business/project-apply.vue | 34 ++- src/views/vehicle/accident-record.vue | 38 ++- .../vehicle/annual-inspection-record.vue | 29 ++- src/views/vehicle/customer-archive.vue | 41 +++- src/views/vehicle/insurance-record.vue | 46 +++- src/views/vehicle/maintenance-plan.vue | 48 +++- src/views/vehicle/mileage-record.vue | 22 +- src/views/vehicle/oil-electric-record.vue | 30 ++- src/views/vehicle/tire-replacement-record.vue | 45 +++- src/views/vehicle/transport-change-record.vue | 17 +- src/views/vehicle/violation-record.vue | 34 ++- 29 files changed, 1150 insertions(+), 250 deletions(-) diff --git a/src/api/business/waybill-manage.js b/src/api/business/waybill-manage.js index 2ab51d8..5c7f99c 100644 --- a/src/api/business/waybill-manage.js +++ b/src/api/business/waybill-manage.js @@ -67,6 +67,16 @@ export const getImportDetails = params => request({ url: `${baseUrl}/import-batc export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } }); export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' }); export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' }); -export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data }); -export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' }); -export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data }); +export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data, timeout: 120000 }); +export const validateImport = data => + request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, timeout: 120000 }); +export const exportImportFailures = data => + request({ + url: `${baseUrl}/import-batch/export-failures`, + method: 'post', + data, + responseType: 'blob', + timeout: 120000, + }); +export const confirmImport = data => + request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 }); diff --git a/src/components/address-map-picker/main.vue b/src/components/address-map-picker/main.vue index ac8800f..3e14d8a 100644 --- a/src/components/address-map-picker/main.vue +++ b/src/components/address-map-picker/main.vue @@ -24,10 +24,12 @@ diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 71ea562..12259ef 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -3173,9 +3173,11 @@ const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; let amapLoader; const standaloneWaybillFormRoute = '/business/waybill-manage/form'; -const standaloneWaybillFormPaths = ['/business/waybill-manage', standaloneWaybillFormRoute]; +const standaloneWaybillListRoute = '/business/waybill-manage'; +const standaloneWaybillFormPaths = [standaloneWaybillListRoute, standaloneWaybillFormRoute]; const standaloneWaybillDetailRoute = '/business/waybill-manage/detail'; const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills'; +const waybillManageListRefreshKey = 'waybill-manage-list-refresh'; const attachmentViewerPlugins = [ imagePlugin(), @@ -3692,6 +3694,14 @@ export default { deactivated() { this.closeInnerDialogs(); }, + activated() { + // 列表实例被 keep-alive 重新激活时,按标记拉取最新数据 + if (this.formPageLocked || this.detailPageLocked) return; + if (sessionStorage.getItem(waybillManageListRefreshKey)) { + sessionStorage.removeItem(waybillManageListRefreshKey); + this.onLoad(this.page, this.query); + } + }, beforeUnmount() { this.closeInnerDialogs(); }, @@ -3999,7 +4009,19 @@ export default { }, watch: { $route(to, from) { - if (!this.isStandaloneWaybillFormPage) return; + // 列表实例:从表单/详情返回时刷新,避免 keep-alive 仍显示旧数据 + if (!this.formPageLocked && !this.detailPageLocked) { + if ( + to.path === standaloneWaybillListRoute && + (from?.path?.includes('/waybill-manage/form') || + from?.path?.includes('/waybill-manage/detail')) + ) { + sessionStorage.removeItem(waybillManageListRefreshKey); + this.onLoad(this.page, this.query); + } + return; + } + if (!this.formPageLocked || !this.isStandaloneWaybillFormPage) return; if ( to.query.mode !== from?.query?.mode || String(to.query.id || '') !== String(from?.query?.id || '') @@ -4708,8 +4730,9 @@ export default { this.waybillTrackInfo = null; this.waybillTrackHint = ''; if (this.isStandaloneWaybillDetailPage) { + this.markWaybillListNeedRefresh(); this.$router.$avueRouter?.closeTag?.(); - this.$router.push({ path: '/business/waybill-manage', query: {} }); + this.$router.push({ path: standaloneWaybillListRoute, query: {} }); return; } this.detailBox = false; @@ -5438,6 +5461,12 @@ export default { if (this.config.saveAsDraft && saveAsDraft) { submitRow[this.config.draftStatusProp || 'approvalStatus'] = this.config.draftStatus || 'draft'; + } else if ( + this.config.draftStatusProp === 'businessStatus' && + submitRow.businessStatus === (this.config.draftStatus || 'draft') + ) { + // 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中 + submitRow.businessStatus = ''; } delete submitRow.basicInfoTitle; delete submitRow.templateInfoTitle; @@ -5675,6 +5704,7 @@ export default { }) : Promise.resolve(); return after.then(() => { + this.markWaybillListNeedRefresh(); this.onLoad(this.page); this.$message({ type: 'success', message: '提交成功!' }); done(); @@ -5713,6 +5743,7 @@ export default { }); return after.then(() => { this.attachmentUploadMode = false; + this.markWaybillListNeedRefresh(); this.onLoad(this.page); this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' }); done(); @@ -5729,6 +5760,9 @@ export default { getSaveRequest() { return this.api.submit; }, + markWaybillListNeedRefresh() { + sessionStorage.setItem(waybillManageListRefreshKey, '1'); + }, handleDraftSave() { const request = typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit; @@ -5739,6 +5773,7 @@ export default { request(submitRow) .then(() => { this.$message.success(`${this.config.draftSaveText || '保存'}成功`); + this.markWaybillListNeedRefresh(); this.onLoad(this.page, this.query); this.closeCrudDialog(); }) @@ -5748,8 +5783,10 @@ export default { }, closeCrudDialog() { if (this.isStandaloneWaybillPage) { + // 关闭独立表单/详情页时标记列表需刷新(含直接点关闭未保存的场景) + this.markWaybillListNeedRefresh(); this.$router.$avueRouter?.closeTag?.(); - this.$router.push({ path: '/business/waybill-manage', query: {} }); + this.$router.push({ path: standaloneWaybillListRoute, query: {} }); return; } const crud = this.$refs.crud; diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 36c3819..2b53bf1 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -1034,6 +1034,11 @@ import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-fil import '@open-file-viewer/core/style.css'; import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url'; +const contractManageListRoute = '/business/contract-manage'; +const contractManageFormRoute = '/business/contract-manage/form'; +const contractManageDetailRoute = '/business/contract-manage/detail'; +const contractManageListRefreshKey = 'contract-manage-list-refresh'; + const clone = value => JSON.parse(JSON.stringify(value)); const normalizeOptionalInteger = (value, emptyValue = null) => { if (value === undefined || value === null || value === '' || Number(value) < 0) { @@ -1296,6 +1301,9 @@ export default { flowBox: false, flowUrl: '', processInstanceId: '', + // 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转 + formPageLocked: false, + detailPageLocked: false, }; }, computed: { @@ -1313,15 +1321,13 @@ export default { return this.hasPermission(`${this.config.permission}_add`); }, isListPage() { - return !this.isFormPage && !this.isDetailPage; + return !this.formPageLocked && !this.detailPageLocked && !this.isPublicViewPage; }, isFormPage() { - return this.$route.path === '/business/contract-manage/form'; + return this.formPageLocked; }, isDetailPage() { - return ( - this.$route.path === '/business/contract-manage/detail' || this.isPublicViewPage - ); + return this.detailPageLocked || this.isPublicViewPage; }, isPublicViewPage() { return this.$route.path === '/business/contract-manage/public-view'; @@ -1484,9 +1490,21 @@ export default { }, }, watch: { - '$route.fullPath'() { - if (this.isFormPage) this.initFormPage(); - if (this.isDetailPage) this.initDetailPage(); + $route(to, from) { + if (this.isListPage) { + if ( + to.path === contractManageListRoute && + (from?.path?.includes('/contract-manage/form') || + from?.path?.includes('/contract-manage/detail') || + from?.path?.includes('/contract-manage/change')) + ) { + sessionStorage.removeItem(contractManageListRefreshKey); + this.onLoad(this.page, this.query); + } + return; + } + if (this.formPageLocked && to.path === contractManageFormRoute) this.initFormPage(); + if (this.detailPageLocked && to.path === contractManageDetailRoute) this.initDetailPage(); }, settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; @@ -1498,6 +1516,9 @@ export default { }, }, created() { + this.formPageLocked = this.$route.path === contractManageFormRoute; + this.detailPageLocked = + this.$route.path === contractManageDetailRoute || this.isPublicViewPage; if (!this.isPublicViewPage) { this.loadProjectOptions(); this.loadSettlementDictionaries(); @@ -1506,7 +1527,17 @@ export default { if (this.isFormPage) this.initFormPage(); if (this.isDetailPage) this.initDetailPage(); }, + activated() { + if (!this.isListPage) return; + if (sessionStorage.getItem(contractManageListRefreshKey)) { + sessionStorage.removeItem(contractManageListRefreshKey); + this.onLoad(this.page, this.query); + } + }, methods: { + markContractManageListNeedRefresh() { + sessionStorage.setItem(contractManageListRefreshKey, '1'); + }, buildTableOption() { return { ...option, @@ -1648,7 +1679,7 @@ export default { }, openForm(mode, row = {}) { this.$router.push({ - path: '/business/contract-manage/form', + path: contractManageFormRoute, query: mode === 'edit' ? { mode, id: row.id, name: '编辑合同管理' } @@ -1741,8 +1772,9 @@ export default { this.$nextTick(() => this.$refs.contractForm?.clearValidate()); }, closeForm() { + this.markContractManageListNeedRefresh(); this.$router.$avueRouter?.closeTag?.(); - this.$router.push('/business/contract-manage'); + this.$router.push(contractManageListRoute); }, validateBase(action) { if (action === 'temporary') { @@ -2532,8 +2564,9 @@ export default { }); }, closeDetail() { + this.markContractManageListNeedRefresh(); this.$router.$avueRouter?.closeTag?.(); - this.$router.push('/business/contract-manage'); + this.$router.push(contractManageListRoute); }, applyDetailState(detail = {}) { this.detailRow = { @@ -2876,7 +2909,7 @@ export default { }, handleCopy(row) { this.$router.push({ - path: '/business/contract-manage/form', + path: contractManageFormRoute, query: { mode: 'add', copyId: row.id, name: '新增合同管理' }, }); }, diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index 7fc4461..80138c7 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -3009,6 +3009,8 @@ export default { this.cargoRows = (cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows)).map( row => sanitizeCargoRow(row) ); + // 运单回填时 routeNodes 尚为空会清空发到货地,路线恢复后需再同步 + this.syncRouteAddressFields(); }, restoreRouteChangeRecords() { try { @@ -3644,7 +3646,8 @@ export default { try { const query = { ...this.buildQueryParams(this.candidateQuery), - businessStatus: 'pending', + // 待执行(需接单未接)与进行中(无需接单或已接单)均可配载 + businessStatus: 'pending,running', onlyUnassignedLoading: 1, // 仅查当前账户所属组织及子组织下的运单 allDept: 0, @@ -3764,7 +3767,10 @@ export default { this.routeNodes = this.buildRouteNodes(rows); } this.cargoRows = this.buildCargoRows(rows); - this.syncRouteAddressFields(); + // 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地 + if (rebuildRoute || this.routeNodes.length) { + this.syncRouteAddressFields(); + } }, buildRouteNodes(rows) { const nodeMap = new Map(); diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index a4a10f4..e436ee7 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -1008,6 +1008,10 @@ const attachmentViewerPlugins = [ fallbackPlugin(), ]; +const projectApplyListRoute = '/business/project-apply'; +const projectApplyFormRoute = '/business/project-apply/form'; +const projectApplyListRefreshKey = 'project-apply-list-refresh'; + const otherAttachmentType = { label: '其他附件', value: '其他附件' }; const normalProjectAttachmentTypeOptions = [ @@ -1367,7 +1371,7 @@ export default { this.loadCargoTypeOptions(); this.loadTransportTypeOptions(); this.loadSettlementModeOptions(); - if (this.$route.path === '/business/project-apply/form') { + if (this.$route.path === projectApplyFormRoute) { this.isFormPageInstance = true; this.openProjectFormPage(); } @@ -1377,10 +1381,32 @@ export default { deactivated() { this.closeInnerDialogs(); }, + activated() { + if (this.isFormPageInstance || this.isPublicViewPage) return; + if (sessionStorage.getItem(projectApplyListRefreshKey)) { + sessionStorage.removeItem(projectApplyListRefreshKey); + this.onLoad(this.page, this.query); + } + }, beforeUnmount() { this.closeInnerDialogs(); }, + watch: { + $route(to, from) { + if (this.isFormPageInstance || this.isPublicViewPage) return; + if ( + to.path === projectApplyListRoute && + from?.path?.includes('/project-apply/form') + ) { + sessionStorage.removeItem(projectApplyListRefreshKey); + this.onLoad(this.page, this.query); + } + }, + }, methods: { + markProjectApplyListNeedRefresh() { + sessionStorage.setItem(projectApplyListRefreshKey, '1'); + }, closeInnerDialogs() { this.changeRecordDetailVisible = false; this.attachmentDocumentPreviewVisible = false; @@ -1506,7 +1532,9 @@ export default { }, closeProjectForm() { if (this.isProjectFormPage) { - this.$router.push('/business/project-apply'); + this.markProjectApplyListNeedRefresh(); + this.$router.$avueRouter?.closeTag?.(); + this.$router.push(projectApplyListRoute); } else { this.projectBox = false; } @@ -1736,7 +1764,7 @@ export default { else if (type === 'view') query.name = `查看${this.config.title}`; else if (type === 'majorSupplement') query.name = '重大项目补录'; else if (type === 'change') query.name = '项目变更'; - this.$router.push({ path: '/business/project-apply/form', query }); + this.$router.push({ path: projectApplyFormRoute, query }); }, resetProjectDialog() { this.$refs.projectForm?.clearValidate(); diff --git a/src/views/vehicle/accident-record.vue b/src/views/vehicle/accident-record.vue index 91b56e0..e5c446f 100644 --- a/src/views/vehicle/accident-record.vue +++ b/src/views/vehicle/accident-record.vue @@ -332,15 +332,34 @@ export default { ? values.vehicleNo.trim() : values.vehicleNo.trim().toUpperCase(); } + // 非必填金额未填写时不要提交 -1 占位值 + values.directEconomicLoss = this.normalizeOptionalAmount(values.directEconomicLoss); + values.insuranceClaimAmount = this.normalizeOptionalAmount(values.insuranceClaimAmount); + values.accidentLocation = this.normalizeOptionalText(values.accidentLocation); + values.accidentReasonDamage = this.normalizeOptionalText(values.accidentReasonDamage); + values.remark = this.normalizeOptionalText(values.remark); values.attachments = this.stringifyAttachments(values.attachments); return values; }, + normalizeOptionalAmount(value) { + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return null; + } + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, validateRow(row) { - if (Number(row.directEconomicLoss) < 0) { + const directEconomicLoss = this.normalizeOptionalAmount(row.directEconomicLoss); + const insuranceClaimAmount = this.normalizeOptionalAmount(row.insuranceClaimAmount); + if (directEconomicLoss !== null && Number(directEconomicLoss) < 0) { this.$message.warning('直接经济损失不能小于 0'); return false; } - if (Number(row.insuranceClaimAmount) < 0) { + if (insuranceClaimAmount !== null && Number(insuranceClaimAmount) < 0) { this.$message.warning('保险理赔金额不能小于 0'); return false; } @@ -353,13 +372,9 @@ export default { return false; } if ( - row.directEconomicLoss !== undefined && - row.directEconomicLoss !== null && - row.directEconomicLoss !== '' && - row.insuranceClaimAmount !== undefined && - row.insuranceClaimAmount !== null && - row.insuranceClaimAmount !== '' && - Number(row.insuranceClaimAmount) > Number(row.directEconomicLoss) + directEconomicLoss !== null && + insuranceClaimAmount !== null && + Number(insuranceClaimAmount) > Number(directEconomicLoss) ) { this.$message.warning('保险理赔金额不能超过直接经济损失金额'); return false; @@ -448,6 +463,11 @@ export default { const detail = res.data.data; detail.attachments = this.parseAttachments(detail.attachments); detail.vehicleType = detail.vehicleType || '车辆'; + detail.directEconomicLoss = this.normalizeOptionalAmount(detail.directEconomicLoss); + detail.insuranceClaimAmount = this.normalizeOptionalAmount(detail.insuranceClaimAmount); + detail.accidentLocation = this.normalizeOptionalText(detail.accidentLocation) || ''; + detail.accidentReasonDamage = this.normalizeOptionalText(detail.accidentReasonDamage) || ''; + detail.remark = this.normalizeOptionalText(detail.remark) || ''; this.form = detail; this.updateVehicleTypeDisplays(detail.vehicleType); this.vehicleOptions = []; diff --git a/src/views/vehicle/annual-inspection-record.vue b/src/views/vehicle/annual-inspection-record.vue index 1a81cd5..961c068 100644 --- a/src/views/vehicle/annual-inspection-record.vue +++ b/src/views/vehicle/annual-inspection-record.vue @@ -367,11 +367,33 @@ export default { } else { values.shipInspectionType = undefined; } + // 费用必填,-1 转 null 后由校验拦截;非必填文本清除 "-1" + values.fee = this.normalizeOptionalAmount(values.fee); + values.inspectionUnit = this.normalizeOptionalText(values.inspectionUnit); + values.assessmentUnit = this.normalizeOptionalText(values.assessmentUnit); + values.passengerTypeLevel = this.normalizeOptionalText(values.passengerTypeLevel); + values.remark = this.normalizeOptionalText(values.remark); values.attachments = this.stringifyAttachments(values.attachments); return values; }, + normalizeOptionalAmount(value) { + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return null; + } + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, validateRow(row) { - if (Number(row.fee) < 0) { + const fee = this.normalizeOptionalAmount(row.fee); + if (fee === null) { + this.$message.warning('请输入费用'); + return false; + } + if (Number(fee) < 0) { this.$message.warning('费用不能小于 0'); return false; } @@ -465,6 +487,11 @@ export default { getDetail(this.form.id).then(res => { const detail = res.data.data; detail.attachments = this.parseAttachments(detail.attachments); + detail.fee = this.normalizeOptionalAmount(detail.fee); + detail.inspectionUnit = this.normalizeOptionalText(detail.inspectionUnit) || ''; + detail.assessmentUnit = this.normalizeOptionalText(detail.assessmentUnit) || ''; + detail.passengerTypeLevel = this.normalizeOptionalText(detail.passengerTypeLevel) || ''; + detail.remark = this.normalizeOptionalText(detail.remark) || ''; this.form = detail; this.updateVehicleTypeDisplays(detail.vehicleType); this.vehicleOptions = []; diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index cefecfc..55575dd 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -1549,6 +1549,10 @@ import SectionCard from '@/components/section-card/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue'; import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue'; +const customerArchiveListRoute = '/vehicle/customer-archive'; +const customerArchiveFormRoute = '/vehicle/customer-archive/form'; +const customerArchiveListRefreshKey = 'customer-archive-list-refresh'; + const createTimeRangeMap = { createTimeRange: ['createTimeStart', 'createTimeEnd'], }; @@ -1646,6 +1650,8 @@ export default { readonly: false, reviewMode: false, listActivated: false, + // 表单页实例标记:创建时按路由落定,避免 keep-alive 缓存后随全局 $route 翻转 + isFormPageInstance: false, originalAccessType: '', activeTab: 'scores', archiveForm: this.emptyArchive(), @@ -2033,6 +2039,7 @@ export default { this.openArchive({ id }, true); return; } + this.isFormPageInstance = this.$route.path === customerArchiveFormRoute; this.initDeptTree(); this.initRegionOptions(); this.initBusinessDictionaries(); @@ -2046,7 +2053,13 @@ export default { } }, activated() { - if (this.isArchivePage || this.isPublicViewPage) return; + if (this.isFormPageInstance || this.isPublicViewPage) return; + if (sessionStorage.getItem(customerArchiveListRefreshKey)) { + sessionStorage.removeItem(customerArchiveListRefreshKey); + this.onLoad(this.page, this.query); + this.listActivated = true; + return; + } if (!this.listActivated) { this.listActivated = true; return; @@ -2065,9 +2078,7 @@ export default { }; }, isArchivePage() { - return ( - this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage - ); + return this.isFormPageInstance || this.isPublicViewPage; }, isPublicViewPage() { return ( @@ -2256,6 +2267,16 @@ export default { }, }, watch: { + $route(to, from) { + if (this.isFormPageInstance || this.isPublicViewPage) return; + if ( + to.path === customerArchiveListRoute && + from?.path?.includes('/customer-archive/form') + ) { + sessionStorage.removeItem(customerArchiveListRefreshKey); + this.onLoad(this.page, this.query); + } + }, 'archiveForm.registeredDetailAddress'() { this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow')); }, @@ -2264,6 +2285,9 @@ export default { }, }, methods: { + markCustomerArchiveListNeedRefresh() { + sessionStorage.setItem(customerArchiveListRefreshKey, '1'); + }, hasPermission(code) { return this.isAdmin || this.validData(this.permission[code], false); }, @@ -4016,27 +4040,28 @@ export default { openArchivePage(row) { const isEdit = Boolean(row?.id); this.$router.push({ - path: '/vehicle/customer-archive/form', + path: customerArchiveFormRoute, query: isEdit ? { id: row.id, name: '编辑客商档案' } : { name: '新增客商档案' }, }); }, viewArchive(row) { this.$router.push({ - path: '/vehicle/customer-archive/form', + path: customerArchiveFormRoute, query: { id: row.id, name: '查看客商档案', view: '1' }, }); }, openReview(row) { this.$router.push({ - path: '/vehicle/customer-archive/form', + path: customerArchiveFormRoute, query: { id: row.id, name: '客商复评', review: '1' }, }); }, closeArchive() { if (this.isPublicViewPage) return; this.archiveBox = false; + this.markCustomerArchiveListNeedRefresh(); this.$router.$avueRouter?.closeTag?.(); - this.$router.push('/vehicle/customer-archive'); + this.$router.push(customerArchiveListRoute); }, openArchive(row, readonly = false) { this.readonly = readonly; diff --git a/src/views/vehicle/insurance-record.vue b/src/views/vehicle/insurance-record.vue index 4c090eb..f8c756e 100644 --- a/src/views/vehicle/insurance-record.vue +++ b/src/views/vehicle/insurance-record.vue @@ -381,8 +381,26 @@ export default { if (values.policyNo) { values.policyNo = values.policyNo.trim(); } + // 非必填项未填写时不要提交 -1 占位值 + values.insuredAmount = this.normalizeOptionalAmount(values.insuredAmount); + values.premium = this.normalizeOptionalAmount(values.premium); + values.invoiceNo = this.normalizeOptionalText(values.invoiceNo); + values.invoiceDate = this.normalizeOptionalText(values.invoiceDate); + values.remark = this.normalizeOptionalText(values.remark); + values.policyFile = this.normalizeOptionalText(values.policyFile); return values; }, + normalizeOptionalAmount(value) { + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return null; + } + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, normalizeDecimalInput(prop, value) { const sanitized = String(value ?? '').replace(/[^\d.]/g, ''); const [integerPart, ...decimalParts] = sanitized.split('.'); @@ -398,8 +416,18 @@ export default { this.$message.warning('结束日期不能早于开始日期'); return false; } - if (Number(row.insuredAmount) < 0 || Number(row.premium) < 0) { - this.$message.warning('保额、保费不能小于 0'); + const insuredAmount = this.normalizeOptionalAmount(row.insuredAmount); + const premium = this.normalizeOptionalAmount(row.premium); + if (insuredAmount !== null && Number(insuredAmount) < 0) { + this.$message.warning('保额不能小于 0'); + return false; + } + if (premium === null) { + this.$message.warning('请输入保费'); + return false; + } + if (Number(premium) < 0) { + this.$message.warning('保费不能小于 0'); return false; } if (row.remark && row.remark.length > 200) { @@ -479,15 +507,25 @@ export default { this.vehicleOptions = []; this.loadVehicleOptions(''); this.form.vehicleType = '车辆'; + this.form.insuredAmount = ''; + this.form.invoiceNo = ''; + this.form.invoiceDate = ''; + this.form.remark = ''; this.initOcrTemplateOptions('车辆', true); } if (['edit', 'view'].includes(type)) { this.isDetailLoading = true; getDetail(this.form.id) .then(res => { + const detail = res.data.data || {}; this.form = { - ...res.data.data, - vehicleType: res.data.data.vehicleType || '车辆', + ...detail, + vehicleType: detail.vehicleType || '车辆', + insuredAmount: this.normalizeOptionalAmount(detail.insuredAmount), + premium: this.normalizeOptionalAmount(detail.premium), + invoiceNo: this.normalizeOptionalText(detail.invoiceNo) || '', + invoiceDate: this.normalizeOptionalText(detail.invoiceDate) || '', + remark: this.normalizeOptionalText(detail.remark) || '', }; this.vehicleOptions = []; this.loadVehicleOptions(this.form.vehicleNo || ''); diff --git a/src/views/vehicle/maintenance-plan.vue b/src/views/vehicle/maintenance-plan.vue index 9c05143..03c6bbf 100644 --- a/src/views/vehicle/maintenance-plan.vue +++ b/src/views/vehicle/maintenance-plan.vue @@ -620,11 +620,11 @@ export default { }); }, formatMileage(value, unit) { - if (value === undefined || value === null || value === '') return ''; + if (value === undefined || value === null || value === '' || Number(value) === -1) return ''; return `${Math.max(Number(value), 0)} ${unit || '公里'}`; }, isUnsetNextMaintenanceMileage(value) { - return Number(value) === -1; + return value === undefined || value === null || value === '' || Number(value) === -1; }, formatNextMaintenanceMileage(value, unit) { if (this.isUnsetNextMaintenanceMileage(value)) return ''; @@ -647,7 +647,7 @@ export default { } }, isEmpty(value) { - return value === undefined || value === null || value === ''; + return value === undefined || value === null || value === '' || Number(value) === -1; }, toNumber(value) { return this.isEmpty(value) ? null : Number(value); @@ -686,12 +686,34 @@ export default { const values = { ...row }; values.vehicleType = values.vehicleType || '车辆'; values.mileageUnit = values.vehicleType === '船舶' ? '海里' : '公里'; + // 非必填里程未填写时不要提交 -1 占位值 + values.mileage = this.normalizeOptionalAmount(values.mileage); + values.nextMaintenanceMileage = this.normalizeOptionalAmount(values.nextMaintenanceMileage); + values.maintainer = this.normalizeOptionalText(values.maintainer); + values.maintenanceItem = this.normalizeOptionalText(values.maintenanceItem); + values.storeName = this.normalizeOptionalText(values.storeName); + values.contactPhone = this.normalizeOptionalText(values.contactPhone); + values.address = this.normalizeOptionalText(values.address); + values.remark = this.normalizeOptionalText(values.remark); values.attachments = this.stringifyAttachments(values.attachments); return values; }, + normalizeOptionalAmount(value) { + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return null; + } + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, validateRow(row) { - const mileage = this.toNumber(row.mileage); - const nextMaintenanceMileage = this.toNumber(row.nextMaintenanceMileage); + const mileage = this.toNumber(this.normalizeOptionalAmount(row.mileage)); + const nextMaintenanceMileage = this.toNumber( + this.normalizeOptionalAmount(row.nextMaintenanceMileage) + ); if ( mileage !== null && nextMaintenanceMileage !== null && @@ -782,12 +804,16 @@ export default { detail.attachments = this.parseAttachments(detail.attachments); detail.vehicleType = detail.vehicleType || '车辆'; detail.mileageUnit = detail.vehicleType === '船舶' ? '海里' : '公里'; - if (type === 'edit') { - if (Number(detail.mileage) === -1) detail.mileage = ''; - if (Number(detail.nextMaintenanceMileage) === -1) { - detail.nextMaintenanceMileage = ''; - } - } + detail.mileage = this.normalizeOptionalAmount(detail.mileage); + detail.nextMaintenanceMileage = this.normalizeOptionalAmount( + detail.nextMaintenanceMileage + ); + detail.maintainer = this.normalizeOptionalText(detail.maintainer) || ''; + detail.maintenanceItem = this.normalizeOptionalText(detail.maintenanceItem) || ''; + detail.storeName = this.normalizeOptionalText(detail.storeName) || ''; + detail.contactPhone = this.normalizeOptionalText(detail.contactPhone) || ''; + detail.address = this.normalizeOptionalText(detail.address) || ''; + detail.remark = this.normalizeOptionalText(detail.remark) || ''; this.form = detail; this.updateVehicleType(detail.vehicleType); this.vehicleOptions = []; diff --git a/src/views/vehicle/mileage-record.vue b/src/views/vehicle/mileage-record.vue index 1343493..c68e527 100644 --- a/src/views/vehicle/mileage-record.vue +++ b/src/views/vehicle/mileage-record.vue @@ -345,7 +345,7 @@ export default { return JSON.stringify(value); }, isEmpty(value) { - return value === undefined || value === null || value === ''; + return value === undefined || value === null || value === '' || Number(value) === -1; }, toNumber(value) { return this.isEmpty(value) ? null : Number(value); @@ -356,6 +356,15 @@ export default { normalizeIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, ''); }, + normalizeOptionalAmount(value) { + if (this.isEmpty(value)) return null; + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, normalizeRow(row) { const values = { ...row }; values.vehicleType = '车辆'; @@ -363,6 +372,12 @@ export default { if (values.vehicleNo) { values.vehicleNo = values.vehicleNo.trim().toUpperCase(); } + // 非必填里程未填写时不要提交 -1 占位值 + values.previousMonthMileage = this.normalizeOptionalAmount(values.previousMonthMileage); + values.currentMonthMileage = this.normalizeOptionalAmount(values.currentMonthMileage); + values.monthlyMileage = this.normalizeOptionalAmount(values.monthlyMileage); + values.totalMileage = this.normalizeOptionalAmount(values.totalMileage); + values.remark = this.normalizeOptionalText(values.remark); const previousMonthMileage = this.toNumber(values.previousMonthMileage); const currentMonthMileage = this.toNumber(values.currentMonthMileage); if ( @@ -503,6 +518,11 @@ export default { detail.attachments = this.parseAttachments(detail.attachments); detail.vehicleType = '车辆'; detail.mileageUnit = '公里'; + detail.previousMonthMileage = this.normalizeOptionalAmount(detail.previousMonthMileage); + detail.currentMonthMileage = this.normalizeOptionalAmount(detail.currentMonthMileage); + detail.monthlyMileage = this.normalizeOptionalAmount(detail.monthlyMileage); + detail.totalMileage = this.normalizeOptionalAmount(detail.totalMileage); + detail.remark = this.normalizeOptionalText(detail.remark) || ''; this.form = detail; this.vehicleOptions = []; this.loadVehicleOptions(this.form.vehicleNo || ''); diff --git a/src/views/vehicle/oil-electric-record.vue b/src/views/vehicle/oil-electric-record.vue index 9fb5fd9..97ffaef 100644 --- a/src/views/vehicle/oil-electric-record.vue +++ b/src/views/vehicle/oil-electric-record.vue @@ -337,7 +337,7 @@ export default { return JSON.stringify(value); }, isEmpty(value) { - return value === undefined || value === null || value === ''; + return value === undefined || value === null || value === '' || Number(value) === -1; }, normalizeDecimalInput(prop, value) { const sanitized = String(value ?? '').replace(/[^\d.]/g, ''); @@ -346,6 +346,15 @@ export default { ? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}` : integerPart; }, + normalizeOptionalAmount(value) { + if (this.isEmpty(value)) return null; + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, normalizeRow(row) { const values = { ...row }; values.vehicleType = values.vehicleType || '车辆'; @@ -359,6 +368,16 @@ export default { if (values.feeType === '充电') { values.oilProduct = undefined; } + // 非必填数值未填写时不要提交 -1;交易金额必填,-1 转 null 后由校验拦截 + values.quantity = this.normalizeOptionalAmount(values.quantity); + values.unitPrice = this.normalizeOptionalAmount(values.unitPrice); + values.transactionAmount = this.normalizeOptionalAmount(values.transactionAmount); + values.balance = this.normalizeOptionalAmount(values.balance); + values.cardNo = this.normalizeOptionalText(values.cardNo); + values.cardHolder = this.normalizeOptionalText(values.cardHolder); + values.station = this.normalizeOptionalText(values.station); + values.oilProduct = this.normalizeOptionalText(values.oilProduct); + values.remark = this.normalizeOptionalText(values.remark); values.attachments = this.stringifyAttachments(values.attachments); return values; }, @@ -472,6 +491,15 @@ export default { const detail = res.data.data; detail.attachments = this.parseAttachments(detail.attachments); detail.vehicleType = detail.vehicleType || '车辆'; + detail.quantity = this.normalizeOptionalAmount(detail.quantity); + detail.unitPrice = this.normalizeOptionalAmount(detail.unitPrice); + detail.transactionAmount = this.normalizeOptionalAmount(detail.transactionAmount); + detail.balance = this.normalizeOptionalAmount(detail.balance); + detail.cardNo = this.normalizeOptionalText(detail.cardNo) || ''; + detail.cardHolder = this.normalizeOptionalText(detail.cardHolder) || ''; + detail.station = this.normalizeOptionalText(detail.station) || ''; + detail.oilProduct = this.normalizeOptionalText(detail.oilProduct) || ''; + detail.remark = this.normalizeOptionalText(detail.remark) || ''; this.form = detail; this.updateVehicleType(detail.vehicleType); this.vehicleOptions = []; diff --git a/src/views/vehicle/tire-replacement-record.vue b/src/views/vehicle/tire-replacement-record.vue index ad88d81..7d4d238 100644 --- a/src/views/vehicle/tire-replacement-record.vue +++ b/src/views/vehicle/tire-replacement-record.vue @@ -298,19 +298,41 @@ export default { if (values.vehicleNo) { values.vehicleNo = values.vehicleNo.trim().toUpperCase(); } + // 非必填数量未填写时不要提交 -1;费用必填,-1 转 null 后由校验拦截 + values.tireQuantity = this.normalizeOptionalAmount(values.tireQuantity); + values.replacementCost = this.normalizeOptionalAmount(values.replacementCost); + values.handler = this.normalizeOptionalText(values.handler); + values.tireBrand = this.normalizeOptionalText(values.tireBrand); + values.replacementDescription = this.normalizeOptionalText(values.replacementDescription); + values.remark = this.normalizeOptionalText(values.remark); values.attachments = this.stringifyAttachments(values.attachments); return values; }, + normalizeOptionalAmount(value) { + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return null; + } + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, validateRow(row) { - if (Number(row.replacementCost) < 0) { + const replacementCost = this.normalizeOptionalAmount(row.replacementCost); + const tireQuantity = this.normalizeOptionalAmount(row.tireQuantity); + if (replacementCost === null) { + this.$message.warning('请输入换胎费用'); + return false; + } + if (Number(replacementCost) < 0) { this.$message.warning('换胎费用不能小于 0'); return false; } if ( - row.tireQuantity !== undefined && - row.tireQuantity !== null && - row.tireQuantity !== '' && - (!Number.isInteger(Number(row.tireQuantity)) || Number(row.tireQuantity) <= 0) + tireQuantity !== null && + (!Number.isInteger(Number(tireQuantity)) || Number(tireQuantity) <= 0) ) { this.$message.warning('换胎数量必须为正整数'); return false; @@ -391,9 +413,16 @@ export default { } if (['edit', 'view'].includes(type)) { getDetail(this.form.id).then(res => { - const detail = res.data.data; - detail.attachments = this.parseAttachments(detail.attachments); - this.form = detail; + const detail = res.data.data; + detail.attachments = this.parseAttachments(detail.attachments); + detail.tireQuantity = this.normalizeOptionalAmount(detail.tireQuantity); + detail.replacementCost = this.normalizeOptionalAmount(detail.replacementCost); + detail.handler = this.normalizeOptionalText(detail.handler) || ''; + detail.tireBrand = this.normalizeOptionalText(detail.tireBrand) || ''; + detail.replacementDescription = + this.normalizeOptionalText(detail.replacementDescription) || ''; + detail.remark = this.normalizeOptionalText(detail.remark) || ''; + this.form = detail; this.vehicleOptions = []; this.loadVehicleOptions(this.form.vehicleNo || ''); }); diff --git a/src/views/vehicle/transport-change-record.vue b/src/views/vehicle/transport-change-record.vue index 140c368..207d344 100644 --- a/src/views/vehicle/transport-change-record.vue +++ b/src/views/vehicle/transport-change-record.vue @@ -289,15 +289,20 @@ export default { normalizeRow(row) { const values = { ...row }; values.vehicleType = values.vehicleType || '车辆'; - if (values.vehicleNo) { - values.vehicleNo = - values.vehicleType === '船舶' - ? values.vehicleNo.trim() - : values.vehicleNo.trim().toUpperCase(); + // 非必填文本未填写时不要提交 "-1" 占位值 + values.vehicleNo = this.normalizeOptionalText(values.vehicleNo); + if (values.vehicleNo && values.vehicleType !== '船舶') { + values.vehicleNo = values.vehicleNo.toUpperCase(); } + values.remark = this.normalizeOptionalText(values.remark); values.attachments = this.stringifyAttachments(values.attachments); return values; }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, validateRow(row) { if (row.changeContent && row.changeContent.length > 200) { this.$message.warning('变更内容不能超过 200 字'); @@ -389,6 +394,8 @@ export default { const detail = res.data.data; detail.attachments = this.parseAttachments(detail.attachments); detail.vehicleType = detail.vehicleType || '车辆'; + detail.vehicleNo = this.normalizeOptionalText(detail.vehicleNo) || ''; + detail.remark = this.normalizeOptionalText(detail.remark) || ''; this.form = detail; this.updateVehicleType(detail.vehicleType); this.vehicleOptions = []; diff --git a/src/views/vehicle/violation-record.vue b/src/views/vehicle/violation-record.vue index bb3d1e6..3e7fa5a 100644 --- a/src/views/vehicle/violation-record.vue +++ b/src/views/vehicle/violation-record.vue @@ -448,9 +448,25 @@ export default { } else { values.violationItem = undefined; } + // 非必填项未填写时不要提交 -1 占位值 + values.fineAmount = this.normalizeOptionalAmount(values.fineAmount); + values.deductPoints = this.normalizeOptionalAmount(values.deductPoints); + values.penaltyUnit = this.normalizeOptionalText(values.penaltyUnit); + values.processResult = this.normalizeOptionalText(values.processResult); values.attachments = this.stringifyAttachments(values.attachments); return values; }, + normalizeOptionalAmount(value) { + if (value === undefined || value === null || value === '' || Number(value) === -1) { + return null; + } + return value; + }, + normalizeOptionalText(value) { + if (value === undefined || value === null) return null; + const text = String(value).trim(); + return !text || text === '-1' ? null : text; + }, validateRow(row) { if (row.vehicleType === '船舶' && !row.violationItem) { this.$message.warning('请输入事项'); @@ -467,17 +483,17 @@ export default { this.$message.warning('日期不能超过当前时间'); return false; } - if (Number(row.fineAmount) < 0) { + const fineAmount = this.normalizeOptionalAmount(row.fineAmount); + const deductPoints = this.normalizeOptionalAmount(row.deductPoints); + if (fineAmount !== null && Number(fineAmount) < 0) { this.$message.warning('被罚金额不能小于 0'); return false; } if ( - row.deductPoints !== undefined && - row.deductPoints !== null && - row.deductPoints !== '' && - (!Number.isInteger(Number(row.deductPoints)) || - Number(row.deductPoints) < 0 || - Number(row.deductPoints) > 15) + deductPoints !== null && + (!Number.isInteger(Number(deductPoints)) || + Number(deductPoints) < 0 || + Number(deductPoints) > 15) ) { this.$message.warning('被扣分数范围为 0-15 分'); return false; @@ -570,6 +586,10 @@ export default { detail.attachments = this.parseAttachments(detail.attachments); detail.vehicleType = detail.vehicleType || '车辆'; detail.processStatus = detail.processStatus || '未处理'; + detail.fineAmount = this.normalizeOptionalAmount(detail.fineAmount); + detail.deductPoints = this.normalizeOptionalAmount(detail.deductPoints); + detail.penaltyUnit = this.normalizeOptionalText(detail.penaltyUnit) || ''; + detail.processResult = this.normalizeOptionalText(detail.processResult) || ''; this.form = detail; this.updateVehicleTypeDisplays(detail.vehicleType); this.vehicleOptions = []; From f3a33b1a047b83d120dedfb9dfe4b79c1f5407ee Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sun, 27 Sep 2026 22:54:33 +0800 Subject: [PATCH 06/16] =?UTF-8?q?:sparkles:=20=E5=AE=8C=E5=96=84=E8=BF=90?= =?UTF-8?q?=E5=8D=95=E5=AF=BC=E5=85=A5=E7=BC=96=E8=BE=91=E5=9B=9E=E5=A1=AB?= =?UTF-8?q?=E3=80=81=E5=88=86=E6=89=B9=E6=A0=A1=E9=AA=8C=E4=B8=8E=E8=BF=90?= =?UTF-8?q?=E8=B4=B9=E5=BD=95=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 导入编辑进入独立页面并回填数据,确认导入按每批100条校验后统一导出失败明细,完整录入运费信息按数量单位联动单价单位。 Co-authored-by: Cursor --- src/api/business/process-config.js | 12 + src/option/business/loading-manage.js | 2 +- src/option/business/waybill-manage.js | 4 +- .../settlement/receivable-payable-detail.js | 2 +- .../components/business-crud-page.vue | 16 +- .../components/master-order-dispatch.vue | 2 +- .../components/transport-plan-page.vue | 10 +- .../components/voucher-execution-detail.vue | 274 ++++++++++++++ .../components/waybill-import-dialog.vue | 339 ++++++++++++++---- .../components/waybill-manage-page.vue | 107 +++--- src/views/business/loading-manage.vue | 26 +- src/views/business/waybill-import-form.vue | 7 +- 12 files changed, 668 insertions(+), 133 deletions(-) create mode 100644 src/views/business/components/voucher-execution-detail.vue diff --git a/src/api/business/process-config.js b/src/api/business/process-config.js index a8aa28e..62fd4e3 100644 --- a/src/api/business/process-config.js +++ b/src/api/business/process-config.js @@ -22,6 +22,18 @@ export const getVoucherImages = (waybillId, params = {}) => ...params, }, }); +export const getVoucherFolders = waybillId => + request({ + url: '/blade-transport/process-config/voucher-folders', + method: 'get', + params: { waybillId }, + }); +export const getVoucherFolderDetail = (voucherId, plateNo) => + request({ + url: '/blade-transport/process-config/voucher-folder-detail', + method: 'get', + params: { voucherId, plateNo }, + }); export const submit = api.submit; export const remove = api.remove; export const copy = api.copy; diff --git a/src/option/business/loading-manage.js b/src/option/business/loading-manage.js index 4fa54d8..4920d79 100644 --- a/src/option/business/loading-manage.js +++ b/src/option/business/loading-manage.js @@ -31,7 +31,7 @@ export const loadingManageConfig = { exportColumns: [ { prop: 'loadingNo', label: '配载单号' }, { prop: 'loadingSubNos', label: '配载子单号' }, - { prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, + { prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' }, { prop: 'driverName', label: '司机' }, { prop: 'driverPhone', label: '联系电话' }, { prop: 'carrierType', label: '承运类型' }, diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js index c981066..e2b9760 100644 --- a/src/option/business/waybill-manage.js +++ b/src/option/business/waybill-manage.js @@ -565,10 +565,10 @@ export const option = { display: false, }, { - label: '车牌号/航班号/船号/班列号', + label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', search: true, - searchLabel: '车/船/航班/班列', + searchLabel: '车牌号/船班号/船次号/班次号', searchOrder: 13, minWidth: 210, display: false, diff --git a/src/option/settlement/receivable-payable-detail.js b/src/option/settlement/receivable-payable-detail.js index 7987665..84f2951 100644 --- a/src/option/settlement/receivable-payable-detail.js +++ b/src/option/settlement/receivable-payable-detail.js @@ -104,7 +104,7 @@ export const generateWaybillColumns = [ { label: '运单号', prop: 'waybillNo', minWidth: 160, link: true }, { label: '项目名称', prop: 'projectName', minWidth: 160 }, { label: '客户/委托方', prop: 'customerName', minWidth: 160 }, - { label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 }, + { label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 }, { label: '司机', prop: 'driverName', minWidth: 120 }, { label: '承运商', prop: 'carrierName', minWidth: 160 }, { label: '运输类型', prop: 'transportType', minWidth: 130 }, diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index 4c2f4f3..7498184 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -2423,7 +2423,7 @@ - - + @@ -3514,7 +3514,7 @@ @@ -6075,7 +6075,7 @@ export default { return this.config.permission === 'waybill_manage' && !this.isRoadTransport; }, transportVehicleNoLabel() { - return this.isRoadTransport ? '车牌号' : '船/航/班列号'; + return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号'; }, transportPlanImportDuplicateCount() { return this.transportPlanImportRows.filter(row => row._duplicate).length; @@ -7121,7 +7121,7 @@ export default { return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商'; }, waybillVehicleNoLabel(row = {}) { - return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号'; + return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号'; }, waybillUnitPrice(row = {}) { const price = row.unitPrice || row.price || ''; @@ -9491,7 +9491,7 @@ export default { } } else { const invalidLengthField = [ - ['vehicleNo', '船/航/班列号', 30], + ['vehicleNo', '船班号/船次号/班次号', 30], ['captainName', '船长', 20], ['cabinNo', '舱位', 30], ['containerNo', '箱号', 30], @@ -13278,7 +13278,7 @@ export default { [ 'vehicleNo', ['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType)) - ? '船/航/班列号' + ? '船班号/船次号/班次号' : '车牌号', ], ]; @@ -13358,7 +13358,7 @@ export default { ['escortPhone', '押运人手机号', 20], ] : [ - ['vehicleNo', '船/航/班列号', 30], + ['vehicleNo', '船班号/船次号/班次号', 30], ['captainName', '船长', 20], ['cabinNo', '舱位', 30], ['containerNo', '箱号', 30], diff --git a/src/views/business/components/master-order-dispatch.vue b/src/views/business/components/master-order-dispatch.vue index 46a18a9..c5efbd2 100644 --- a/src/views/business/components/master-order-dispatch.vue +++ b/src/views/business/components/master-order-dispatch.vue @@ -248,7 +248,7 @@ - + @@ -1401,7 +1401,7 @@ @@ -8123,7 +8123,7 @@ export default { [ 'vehicleNo', ['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType)) - ? '船/航/班列号' + ? '船班号/船次号/班次号' : '车牌号', ], ]; @@ -8204,7 +8204,7 @@ export default { ['escortPhone', '押运人手机号', 20], ] : [ - ['vehicleNo', '船/航/班列号', 30], + ['vehicleNo', '船班号/船次号/班次号', 30], ['captainName', '船长', 20], ['cabinNo', '舱位', 30], ['containerNo', '箱号', 30], diff --git a/src/views/business/components/voucher-execution-detail.vue b/src/views/business/components/voucher-execution-detail.vue new file mode 100644 index 0000000..0de4939 --- /dev/null +++ b/src/views/business/components/voucher-execution-detail.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/views/business/components/waybill-import-dialog.vue b/src/views/business/components/waybill-import-dialog.vue index 2e6adcc..2bc027e 100644 --- a/src/views/business/components/waybill-import-dialog.vue +++ b/src/views/business/components/waybill-import-dialog.vue @@ -202,7 +202,7 @@ - + + - + +
项目信息
@@ -444,16 +447,21 @@ +
导入运单明细
+

+ 请先选择项目、客户合同和承运信息,再导入运单明细 +

添加附件导入请上传运单明细表,仅支持 excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法下载模板 @@ -479,6 +487,7 @@ :data="previewRows" border max-height="440" + :row-class-name="({ row }) => (row._duplicate ? 'is-duplicate-row' : '')" @selection-change="rowSelection = $event" >{{ formatCell(row, column) }}{{ formatCell(row, column) }} @@ -586,7 +600,7 @@ diff --git a/src/views/wel/components/section-business.vue b/src/views/wel/components/section-business.vue new file mode 100644 index 0000000..9322f72 --- /dev/null +++ b/src/views/wel/components/section-business.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/src/views/wel/components/section-finance.vue b/src/views/wel/components/section-finance.vue new file mode 100644 index 0000000..59a6dfd --- /dev/null +++ b/src/views/wel/components/section-finance.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/views/wel/components/section-fleet.vue b/src/views/wel/components/section-fleet.vue new file mode 100644 index 0000000..19cc1c3 --- /dev/null +++ b/src/views/wel/components/section-fleet.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/views/wel/components/section-kpi.vue b/src/views/wel/components/section-kpi.vue new file mode 100644 index 0000000..074fcd6 --- /dev/null +++ b/src/views/wel/components/section-kpi.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/src/views/wel/components/section-project.vue b/src/views/wel/components/section-project.vue new file mode 100644 index 0000000..a1ad6e2 --- /dev/null +++ b/src/views/wel/components/section-project.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/views/wel/components/section-structure.vue b/src/views/wel/components/section-structure.vue new file mode 100644 index 0000000..d8e3504 --- /dev/null +++ b/src/views/wel/components/section-structure.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/views/wel/components/section-summary.vue b/src/views/wel/components/section-summary.vue new file mode 100644 index 0000000..7891a0a --- /dev/null +++ b/src/views/wel/components/section-summary.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/views/wel/components/section-vehicle.vue b/src/views/wel/components/section-vehicle.vue new file mode 100644 index 0000000..528f6b3 --- /dev/null +++ b/src/views/wel/components/section-vehicle.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/views/wel/components/wel-chart.vue b/src/views/wel/components/wel-chart.vue new file mode 100644 index 0000000..2be838b --- /dev/null +++ b/src/views/wel/components/wel-chart.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/views/wel/components/wel-filters.vue b/src/views/wel/components/wel-filters.vue new file mode 100644 index 0000000..3af1636 --- /dev/null +++ b/src/views/wel/components/wel-filters.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/views/wel/dashboard.scss b/src/views/wel/dashboard.scss new file mode 100644 index 0000000..519aa48 --- /dev/null +++ b/src/views/wel/dashboard.scss @@ -0,0 +1,96 @@ +/* 首页看板公共样式:各板块组件 scoped 引入 */ +.wel-card { + background: #ffffff; + border-radius: 10px; + padding: 16px 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.wel-sec__head { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 14px; +} + +.wel-sec__title { + font-size: 15px; + font-weight: 600; + color: #303133; +} + +.wel-filter { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.wel-filter__label { + font-size: 13px; + color: #909399; + white-space: nowrap; +} + +.wel-filter .el-select { + width: 120px; +} + +.panel-title { + display: flex; + align-items: baseline; + gap: 6px; + font-size: 14px; + font-weight: 600; + color: #303133; + margin-bottom: 10px; + + .panel-title__unit { + font-size: 12px; + font-weight: 400; + color: #909399; + } +} + +.mini-card { + background: #ffffff; + border-radius: 10px; + padding: 14px 18px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + + .mini-card__label { + font-size: 13px; + color: #909399; + margin-bottom: 6px; + } + + .mini-card__value { + font-size: 22px; + font-weight: 700; + color: #303133; + line-height: 1.2; + } + + .mini-card__unit { + font-size: 12px; + font-weight: 400; + color: #909399; + margin-left: 4px; + } + + .mini-card__trend { + font-size: 12px; + margin-top: 4px; + color: #909399; + + &.is-up { + color: #f04747; + } + + &.is-down { + color: #2ba471; + } + } +} diff --git a/src/views/wel/index.vue b/src/views/wel/index.vue index 1f3a13e..5ceea6a 100644 --- a/src/views/wel/index.vue +++ b/src/views/wel/index.vue @@ -1,788 +1,56 @@