diff --git a/src/views/business/common-cargo.vue b/src/views/business/common-cargo.vue index ef871c0..818b33c 100644 --- a/src/views/business/common-cargo.vue +++ b/src/views/business/common-cargo.vue @@ -280,6 +280,23 @@ export default { .filter(Boolean)[0] || ''; }, applyDefaultDeptSearch() { + // 超管默认不限组织,避免预填当前组织后看不到其他组织/无组织数据。 + if (this.isAdmin) { + const deptColumn = this.findColumn(this.option.column, 'searchDeptId'); + if (deptColumn) { + deptColumn.searchValue = undefined; + } + const nextQuery = { ...this.query }; + delete nextQuery.deptId; + delete nextQuery.searchDeptId; + this.query = nextQuery; + this.$nextTick(() => { + if (this.$refs.crud?.searchForm) { + this.$refs.crud.searchForm.searchDeptId = undefined; + } + }); + return; + } const deptId = this.defaultDeptId(); const deptColumn = this.findColumn(this.option.column, 'searchDeptId'); if (!deptId || !deptColumn) return; @@ -298,7 +315,8 @@ export default { if (deptColumn) { deptColumn.dicData = this.deptOptions; } - // dicData 异步到位后再回填,避免下拉选项为空时清空已选组织,导致请求丢失 deptId。 + // 超管不回填默认组织;普通用户在 dicData 到位后再回填,避免下拉清空已选组织。 + if (this.isAdmin) return; const deptId = String(this.query.deptId || this.defaultDeptId() || ''); if (!deptId) return; if (deptColumn) { diff --git a/src/views/business/components/master-order-editor.vue b/src/views/business/components/master-order-editor.vue index 6ad4fde..0417c0e 100644 --- a/src/views/business/components/master-order-editor.vue +++ b/src/views/business/components/master-order-editor.vue @@ -652,6 +652,8 @@ export default { addressQuery: {}, addressPage: { current: 1, size: 10, total: 0 }, addressRegionPaths: { departure: [], arrival: [] }, + // 程序回填省市区时抑制 cascader @change,避免空值回调把已填地址清空 + suppressAddressRegionChange: false, regionOptions: [], regionLoading: false, regionRequest: null, @@ -889,26 +891,63 @@ export default { findRegionPathByName(options = [], regionName = '', parents = []) { const target = String(regionName || '').replace(/\s+/g, ''); if (!target) return []; + let suffixMatch = []; for (const item of options) { const nextParents = [...parents, item]; const name = nextParents.map(region => region.title || region.name || '').join(''); if (name === target) return nextParents.map(region => region.id); + // 常用地址可能缺省名,允许「市+区」后缀命中完整省市区路径 + if (!suffixMatch.length && name.endsWith(target) && target.length >= 2) { + suffixMatch = nextParents.map(region => region.id); + } const path = this.findRegionPathByName(item.children, target, nextParents); if (path.length) return path; } - return []; + return suffixMatch; }, findRegionPathByCode(options = [], regionCode = '', parents = []) { const target = String(regionCode || '').trim(); if (!target) return []; for (const item of options) { const nextParents = [...parents, item]; - if (String(item.id) === target) return nextParents.map(region => region.id); + if (String(item.id) === target || String(item.value || '') === target) { + return nextParents.map(region => region.id); + } const path = this.findRegionPathByCode(item.children, target, nextParents); if (path.length) return path; } return []; }, + resolveRegionPath(field) { + if (!field) return []; + const codePath = this.findRegionPathByCode( + this.regionOptions, + field.model[field.regionCode] + ); + if (codePath.length) return codePath; + const namePath = this.findRegionPathByName(this.regionOptions, field.model[field.name]); + if (namePath.length) return namePath; + const composed = [field.model[field.provinceName], field.model[field.cityName], field.model[field.districtName]] + .filter(Boolean) + .join(''); + return this.findRegionPathByName(this.regionOptions, composed); + }, + setAddressRegionPath(target, path = []) { + const nextPath = Array.isArray(path) ? path.map(item => String(item)) : []; + const current = this.addressRegionPaths[target] || []; + if ( + current.length === nextPath.length && + current.every((item, index) => String(item) === String(nextPath[index])) + ) { + return; + } + this.suppressAddressRegionChange = true; + // Vue3 直接写动态 key,避免整体替换对象导致其它 cascader 重建并触发空 @change + this.addressRegionPaths[target] = nextPath; + this.$nextTick(() => { + this.suppressAddressRegionChange = false; + }); + }, regionLabels(path = []) { let options = this.regionOptions; const labels = []; @@ -947,10 +986,22 @@ export default { this.setAddressRegionNames(field, [provinceMatch?.[0], cityMatch?.[0], districtMatch?.[0]]); }, handleAddressRegionChange(target, value) { + if (this.suppressAddressRegionChange) return; const field = this.addressModel(target); const path = Array.isArray(value) ? value.map(item => String(item)) : []; - this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path }; + this.setAddressRegionPath(target, path); if (!field) return; + // 程序回填/重建过程中 cascader 可能短暂抛出空值,不能据此清空已选常用地址 + if (!path.length) { + const hasExisting = + String(field.model[field.name] || '').trim() || + String(field.model[field.address] || '').trim() || + String(field.model[field.regionCode] || '').trim(); + if (hasExisting) { + this.$nextTick(() => this.syncAddressRegionPath(target)); + return; + } + } const labels = this.regionLabels(path); field.model[field.name] = labels.join(''); field.model[field.regionCode] = path.at(-1) || ''; @@ -959,19 +1010,29 @@ export default { }, syncAddressRegionPath(target) { const field = this.addressModel(target); - if (!field) return; - const codePath = this.findRegionPathByCode(this.regionOptions, field.model[field.regionCode]); - const path = codePath.length - ? codePath - : this.findRegionPathByName(this.regionOptions, field.model[field.name]); - this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path }; + if (!field || !this.regionOptions.length) return; + const path = this.resolveRegionPath(field); + this.setAddressRegionPath(target, path); if (path.length) this.setAddressRegionNames(field, this.regionLabels(path)); }, syncAddressRegionPaths() { if (!this.regionOptions.length) return; - ['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach(target => { - this.syncAddressRegionPath(target); - }); + this.suppressAddressRegionChange = true; + try { + ['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach( + target => { + const field = this.addressModel(target); + if (!field) return; + const path = this.resolveRegionPath(field); + this.addressRegionPaths[target] = path; + if (path.length) this.setAddressRegionNames(field, this.regionLabels(path)); + } + ); + } finally { + this.$nextTick(() => { + this.suppressAddressRegionChange = false; + }); + } }, async loadDetail(id) { const res = await api.getDetail(id); @@ -1233,8 +1294,9 @@ export default { this.stationPage.current = 1; this.loadStationList(); }, - selectStation(row) { - const target = this.addressModel(this.addressTarget); + async selectStation(row) { + const targetKey = this.addressTarget; + const target = this.addressModel(targetKey); if (!target) return; const station = this.normalizeStation(row); target.model[target.name] = station.stationName || station.regionName; @@ -1245,7 +1307,9 @@ export default { target.model[target.siteCode] = station.stationCode; this.setAddressRegionNamesFromRow(target, station); this.stationDialogVisible = false; - this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget)); + await this.loadRegionOptions(); + await this.$nextTick(); + this.syncAddressRegionPath(targetKey); }, async loadAddressList() { this.addressLoading = true; @@ -1271,8 +1335,9 @@ export default { this.addressPage.current = 1; this.loadAddressList(); }, - selectAddress(row) { - const target = this.addressModel(this.addressTarget); + async selectAddress(row) { + const targetKey = this.addressTarget; + const target = this.addressModel(targetKey); if (!target) return; target.model[target.name] = row.regionName || row.addressName || ''; target.model[target.address] = row.detailAddress || row.address || ''; @@ -1284,7 +1349,10 @@ export default { target.model[target.siteCode] = ''; this.setAddressRegionNamesFromRow(target, row); this.addressDialogVisible = false; - this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget)); + await this.loadRegionOptions(); + // nextTick 确保途经点动态 key 已挂载 cascader 后再回填路径 + await this.$nextTick(); + this.syncAddressRegionPath(targetKey); }, handleAddressDetailClick(target) { if (this.isBasicRouteReadonly) return; @@ -1803,12 +1871,23 @@ export default { }, addRoute() { if (this.isBasicRouteReadonly) return; - this.form.routes.push(this.createRoute()); + const index = this.form.routes.length; + this.form.routes.push(this.createRoute(index)); + this.addressRegionPaths[`route-${index}`] = []; }, removeRoute(index) { if (this.isBasicRouteReadonly) return; this.form.routes.splice(index, 1); - this.syncAddressRegionPaths(); + const nextPaths = { departure: this.addressRegionPaths.departure || [], arrival: this.addressRegionPaths.arrival || [] }; + this.form.routes.forEach((route, routeIndex) => { + nextPaths[`route-${routeIndex}`] = this.addressRegionPaths[`route-${routeIndex >= index ? routeIndex + 1 : routeIndex}`] || []; + }); + this.suppressAddressRegionChange = true; + this.addressRegionPaths = nextPaths; + this.$nextTick(() => { + this.suppressAddressRegionChange = false; + this.syncAddressRegionPaths(); + }); }, goodsKey(row = {}) { return `${row.cargoName || ''}\u0000${row.cargoType || ''}`; diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index 340f404..5be2599 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -4023,7 +4023,7 @@ export default { exists.add(key); } }); - // 承运商类型:新增后须仍存在共同的承运商合同乙方(按客户合同甲方匹配后取交集) + // 承运商类型:新增后须仍存在共同的承运商合同乙方(按客户合同乙方=承运商合同甲方匹配后取交集) if ( String(this.dialogForm.carrierType || '').trim() === '承运商' && nextRows.length > this.selectedWaybillRows.length @@ -4744,7 +4744,7 @@ export default { } this.taskCarrierLoading = true; try { - // 配载单只有一个项目;传入子运单后按客户合同甲方匹配并取承运商合同乙方交集 + // 配载单只有一个项目;传入子运单后按客户合同乙方=承运商合同甲方匹配并取承运商合同乙方交集 const projectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean); const waybillIds = this.selectedWaybillRows.map(row => row.id).filter(Boolean); const response = await loadingApi.getCarrierContracts( diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index ff81715..3d9034d 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -249,35 +249,57 @@ /> - + + + + + + - - - + + + + + + + String(item.value) === String(id)); + if (!node) break; + labels.push(node.label); + nodes = node.children; + } + if (labels.length) return labels.join(' / '); + } + return String(this.form.businessDeptName || '').trim(); + }, + undertakeDeptTooltipText() { + const name = String(this.form.undertakeDeptName || '').trim(); + if (name) return name; + return this.resolveUndertakeDeptNames(this.parseUndertakeDeptIds(this.form.undertakeDeptId)); + }, + shouldShowBusinessDeptTooltip() { + return this.dialogReadonly && !!this.businessDeptTooltipText; + }, + shouldShowUndertakeDeptTooltip() { + return this.dialogReadonly && !!this.undertakeDeptTooltipText; + }, permissionList() { return { addBtn: this.canCreate, @@ -3272,6 +3321,20 @@ export default { width: 100%; } + &__field-tooltip-trigger { + display: block; + width: 100%; + } + + &__field-tooltip { + max-width: 480px; + max-height: 240px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.5; + } + :deep(.el-form-item) { //margin-bottom: 24px; } diff --git a/src/views/settlement/components/formal-settlement-editor.vue b/src/views/settlement/components/formal-settlement-editor.vue index dd85e42..bb59b40 100644 --- a/src/views/settlement/components/formal-settlement-editor.vue +++ b/src/views/settlement/components/formal-settlement-editor.vue @@ -3222,10 +3222,47 @@ export default { return files.length ? JSON.stringify(files) : ''; }, parseFeeItems(value) { + const raw = this.parseRawFeeItems(value); + const metaKeys = new Set([ + 'currency', + 'totalFreightAmount', + 'otherFreightAmount', + 'freightItems', + 'freightTotal', + 'totalFreight', + 'totalAmount', + 'freight', + 'freightAmount', + 'transportFee', + 'otherFeeTotal', + 'otherAmount', + ]); + const result = {}; + let otherFeeAmount = 0; + Object.entries(raw).forEach(([key, amount]) => { + if (!key) return; + if (key === 'otherFreightAmount' || key === 'otherFeeTotal' || key === 'otherAmount') { + otherFeeAmount += Number(amount || 0); + return; + } + if (metaKeys.has(key)) return; + if (amount && typeof amount === 'object') return; + result[key] = amount; + }); + const hasOtherFee = Object.keys(result).some( + name => String(name).includes('其它费') || String(name).includes('其他费') + ); + if (otherFeeAmount && !hasOtherFee) { + result['其它费用'] = otherFeeAmount; + } + return result; + }, + parseRawFeeItems(value) { if (!value) return {}; - if (typeof value === 'object') return value; + if (typeof value === 'object') return { ...value }; try { - return JSON.parse(value); + const parsed = JSON.parse(value); + return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index cf92c7b..5dfb517 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -3061,6 +3061,42 @@ export default { }); }, parseFeeItems(value) { + const raw = this.parseRawFeeItems(value); + const metaKeys = new Set([ + 'currency', + 'totalFreightAmount', + 'otherFreightAmount', + 'freightItems', + 'freightTotal', + 'totalFreight', + 'totalAmount', + 'freight', + 'freightAmount', + 'transportFee', + 'otherFeeTotal', + 'otherAmount', + ]); + const result = {}; + let otherFeeAmount = 0; + Object.entries(raw).forEach(([key, amount]) => { + if (!key) return; + if (key === 'otherFreightAmount' || key === 'otherFeeTotal' || key === 'otherAmount') { + otherFeeAmount += Number(amount || 0); + return; + } + if (metaKeys.has(key)) return; + if (amount && typeof amount === 'object') return; + result[key] = amount; + }); + const hasOtherFee = Object.keys(result).some( + name => String(name).includes('其它费') || String(name).includes('其他费') + ); + if (otherFeeAmount && !hasOtherFee) { + result['其它费用'] = otherFeeAmount; + } + return result; + }, + parseRawFeeItems(value) { if (!value) return {}; if (typeof value === 'object') return { ...value }; try {