diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index 685ceb2..0992ceb 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -267,7 +267,7 @@ export const option = createCrudOption([ searchType: 'select', searchPlaceholder: '请选择', formslot: true, - order: 140, + order: 165, dicUrl: '/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved', dicFormatter: listDicFormatter, @@ -345,14 +345,9 @@ export const option = createCrudOption([ label: '甲方', prop: 'partyA', type: 'select', + formslot: true, + disabled: true, order: 160, - dicUrl: - '/blade-transport/customer-archive/list?current=1&size=9999&customerType=客户&approvalStatus=approved&status=1', - dicFormatter: listDicFormatter, - props: { - label: 'fullName', - value: 'fullName', - }, filterable: true, minWidth: 160, rules: selectRule('甲方'), @@ -361,14 +356,9 @@ export const option = createCrudOption([ label: '乙方', prop: 'partyB', type: 'select', + formslot: true, + disabled: true, order: 150, - dicUrl: - '/blade-transport/customer-archive/list?current=1&size=9999&customerType=承运商&approvalStatus=approved&status=1', - dicFormatter: listDicFormatter, - props: { - label: 'fullName', - value: 'fullName', - }, filterable: true, minWidth: 160, rules: selectRule('乙方'), diff --git a/src/option/business/shipping-template.js b/src/option/business/shipping-template.js index 43823f2..598b263 100644 --- a/src/option/business/shipping-template.js +++ b/src/option/business/shipping-template.js @@ -40,6 +40,9 @@ export const config = { projectQueryParams: { approvalStatuses: 'approved,change_approved', }, + contractQueryParams: { + contractCategory: '客户合同', + }, excludeVoidedProjects: true, enableAttachmentTable: true, detailAttachmentDescriptionPlain: true, diff --git a/src/option/business/transport-plan.js b/src/option/business/transport-plan.js index 96c1365..d1475df 100644 --- a/src/option/business/transport-plan.js +++ b/src/option/business/transport-plan.js @@ -167,6 +167,9 @@ export const config = { projectQueryParams: { approvalStatuses: 'approved,change_approved', }, + contractQueryParams: { + contractCategory: '客户合同', + }, templateCreateTarget: 'transport-plan', enableAttachmentTable: true, enableTransportPlanForm: true, diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js index d0e80fc..c788ae5 100644 --- a/src/option/business/waybill-manage.js +++ b/src/option/business/waybill-manage.js @@ -329,6 +329,9 @@ export const config = { projectQueryParams: { approvalStatuses: 'approved,change_approved', }, + contractQueryParams: { + contractCategory: '客户合同', + }, templateCreateTarget: 'waybill-manage', enableContractSelect: true, enableShippingPlanSelect: true, diff --git a/src/views/business/components/billing-plan-editor.vue b/src/views/business/components/billing-plan-editor.vue index 9de5f4c..7fd95fb 100644 --- a/src/views/business/components/billing-plan-editor.vue +++ b/src/views/business/components/billing-plan-editor.vue @@ -18,7 +18,7 @@ rangeUnitPriceInput(row, rangeIndex, value)" /> decimalInput(row, 'unitPrice', value)" /> limitInput(row, rangeIndex, 'lowerLimit', value)" /> limitInput(row, rangeIndex, 'upperLimit', value)" />添加删除 - decimalInput(row, 'minimumBillingWeight', value)" /> + rangeMinimumInput(row, rangeIndex, value)" /> decimalInput(row, 'minimumBillingWeight', value)" /> {{ readonly ? '查看匹配条件' : '设置匹配条件' }}复制删除 @@ -45,7 +45,7 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region'; import { getDictionary } from '@/api/system/dictbiz'; const clone = value => JSON.parse(JSON.stringify(value)); -const defaultRule = () => ({ feeType: '', feeItem: '', billingElement: '', billingType: '', billingUnit: '', unitPrice: '', lowerLimit: '', upperLimit: '', limitRanges: [{ lowerLimit: '', upperLimit: '', unitPrice: '' }], minimumBillingWeight: '', remark: '', matchCondition: { origin: '', originCode: '', originPath: [], destination: '', destinationCode: '', destinationPath: [], transportMode: '', cargoType: '', cargoTypeCode: '', cargoTypePath: [] } }); +const defaultRule = () => ({ feeType: '', feeItem: '', billingElement: '', billingType: '', billingUnit: '', unitPrice: '', lowerLimit: '', upperLimit: '', limitRanges: [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }], minimumBillingWeight: '', remark: '', matchCondition: { origin: '', originCode: '', originPath: [], destination: '', destinationCode: '', destinationPath: [], transportMode: '', cargoType: '', cargoTypeCode: '', cargoTypePath: [] } }); export default { props: { modelValue: Boolean, value: { type: Object, default: () => ({}) }, index: { type: Number, default: -1 }, readonly: Boolean }, @@ -56,9 +56,9 @@ export default { watch: { value: { deep: true, handler(value) { if (this.modelValue) { this.draft = this.normalizePlan(value); this.loadRuleItems(); } } } }, methods: { normalizePlan(value = {}) { const plan = { planName: '', defaultPlan: false, remark: '', ...clone(value || {}) }; plan.rules = (value?.rules?.length ? value.rules : [defaultRule()]).map(rule => this.normalizeRule(rule)); return plan; }, - normalizeRule(rule = {}) { const next = { ...defaultRule(), ...clone(rule) }; next.limitRanges = this.normalizeRanges(next); this.syncLegacyLimit(next); next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) }; return next; }, - normalizeRanges(row) { const ranges = Array.isArray(row.limitRanges) ? row.limitRanges.map(item => ({ lowerLimit: item?.lowerLimit ?? '', upperLimit: item?.upperLimit ?? '', unitPrice: item?.unitPrice === undefined || item?.unitPrice === '' ? row.unitPrice ?? '' : item.unitPrice })).filter(item => item.lowerLimit !== '' || item.upperLimit !== '' || item.unitPrice !== '') : []; if (!ranges.length && (row.lowerLimit !== '' || row.upperLimit !== '' || row.unitPrice !== '')) ranges.push({ lowerLimit: row.lowerLimit ?? '', upperLimit: row.upperLimit ?? '', unitPrice: row.unitPrice ?? '' }); return ranges.length ? ranges : [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; }, - syncLegacyLimit(row) { const first = row.limitRanges?.[0] || {}; row.lowerLimit = first.lowerLimit || ''; row.upperLimit = first.upperLimit || ''; if (this.usesRangeUnitPrice(row)) row.unitPrice = first.unitPrice || ''; }, + normalizeRule(rule = {}) { const next = { ...defaultRule(), ...clone(rule) }; if (next.billingType === '阶梯单价' && next.minimumBillingWeight === '') { const rangeMinimum = (Array.isArray(next.limitRanges) ? next.limitRanges : []).find(item => item?.minimumBillingWeight !== '')?.minimumBillingWeight; if (rangeMinimum !== undefined) next.minimumBillingWeight = rangeMinimum; } next.limitRanges = this.normalizeRanges(next); if (!this.usesRangeMinimum(next)) next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' })); this.syncLegacyLimit(next); next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) }; return next; }, + normalizeRanges(row) { const ranges = Array.isArray(row.limitRanges) ? row.limitRanges.map(item => ({ lowerLimit: item?.lowerLimit ?? '', upperLimit: item?.upperLimit ?? '', unitPrice: item?.unitPrice === undefined || item?.unitPrice === '' ? row.unitPrice ?? '' : item.unitPrice, minimumBillingWeight: item?.minimumBillingWeight === undefined || item?.minimumBillingWeight === '' ? (this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '') : item.minimumBillingWeight })).filter(item => item.lowerLimit !== '' || item.upperLimit !== '' || item.unitPrice !== '' || item.minimumBillingWeight !== '') : []; if (!ranges.length && (row.lowerLimit !== '' || row.upperLimit !== '' || row.unitPrice !== '' || row.minimumBillingWeight !== '')) ranges.push({ lowerLimit: row.lowerLimit ?? '', upperLimit: row.upperLimit ?? '', unitPrice: row.unitPrice ?? '', minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '' }); return ranges.length ? ranges : [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }]; }, + syncLegacyLimit(row) { const first = row.limitRanges?.[0] || {}; row.lowerLimit = first.lowerLimit || ''; row.upperLimit = first.upperLimit || ''; if (this.usesRangeUnitPrice(row)) row.unitPrice = first.unitPrice || ''; if (this.usesRangeMinimum(row)) row.minimumBillingWeight = first.minimumBillingWeight || ''; }, loadDictionaries() { this.feeCategoryLoading = true; getDictionary({ code: 'fee_category' }).then(res => { this.feeCategories = res.data?.data || []; this.loadRuleItems(); }).finally(() => { this.feeCategoryLoading = false; }); this.unitLoading = true; getDictionary({ code: 'unit_fee' }).then(res => { this.unitOptions = res.data?.data || []; }).finally(() => { this.unitLoading = false; }); }, feeTypeKey(row) { const option = this.feeCategories.find(item => String(item.dictKey) === String(row.feeType) || String(item.dictValue) === String(row.feeType)); return option?.dictKey || row.feeType || ''; }, feeItemOptions(row) { return this.feeItems[this.feeTypeKey(row)] || []; }, @@ -66,23 +66,26 @@ export default { loadFeeItems(value) { const key = this.feeTypeKey({ feeType: value }); if (!key || this.feeItems[key]) return; this.feeItemLoadingMap[key] = true; getFeeItemList(1, 9999, { feeCategory: key }).then(res => { const data = res?.data?.data || res?.data || {}; this.feeItems[key] = Array.isArray(data) ? data : (data.records || []); }).finally(() => { this.feeItemLoadingMap[key] = false; }); }, handleFeeTypeChange(row, value) { row.feeType = this.feeTypeKey({ feeType: value }); row.feeItem = ''; this.loadFeeItems(row.feeType); }, billingTypes(row) { return this.typeMap[row.billingElement] || []; }, - handleElementChange(row) { if (!this.billingTypes(row).includes(row.billingType)) row.billingType = ''; if (!this.canEditMinimum(row)) row.minimumBillingWeight = ''; this.handleTypeChange(row); }, - handleTypeChange(row) { if (!this.canEditLimit(row)) { row.limitRanges = [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; row.lowerLimit = ''; row.upperLimit = ''; } else { row.limitRanges = this.normalizeRanges(row); this.syncLegacyLimit(row); } }, + handleElementChange(row) { if (!this.billingTypes(row).includes(row.billingType)) row.billingType = ''; if (!this.canEditMinimum(row)) { row.minimumBillingWeight = ''; row.limitRanges = (row.limitRanges || []).map(item => ({ ...item, minimumBillingWeight: '' })); } this.handleTypeChange(row); }, + handleTypeChange(row) { if (!this.canEditMinimum(row)) row.minimumBillingWeight = ''; if (!this.usesRangeMinimum(row)) row.limitRanges = (row.limitRanges || []).map(item => ({ ...item, minimumBillingWeight: '' })); if (!this.canEditLimit(row)) { row.limitRanges = [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }]; row.lowerLimit = ''; row.upperLimit = ''; } else { row.limitRanges = this.normalizeRanges(row); this.syncLegacyLimit(row); } }, canEditLimit(row) { return !this.readonly && Boolean(row.billingElement) && Boolean(row.billingType) && (row.billingType.includes('区间') || row.billingType.includes('阶梯')); }, - usesRangeUnitPrice(row) { return ['区间单价', '阶梯单价'].includes(row.billingType); }, - canEditMinimum(row) { return !this.readonly && ['按重量', '按吨·公里'].includes(row.billingElement); }, - getRanges(row) { return Array.isArray(row.limitRanges) && row.limitRanges.length ? row.limitRanges : [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; }, - addRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index + 1, 0, { lowerLimit: '', upperLimit: '', unitPrice: '' }); }, - removeRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index, 1); if (!row.limitRanges.length) row.limitRanges.push({ lowerLimit: '', upperLimit: '', unitPrice: '' }); this.syncLegacyLimit(row); }, + usesRangeUnitPrice(row) { return ['区间单价', '阶梯单价', '区间阶梯一口价'].includes(row.billingType); }, + usesRangeMinimum(row) { return this.supportsMinimum(row) && row.billingType === '区间单价'; }, + supportsMinimum(row) { return ['按重量', '按吨·公里'].includes(row.billingElement) && Boolean(row.billingType) && !row.billingType.includes('一口价'); }, + canEditMinimum(row) { return !this.readonly && this.supportsMinimum(row); }, + getRanges(row) { return Array.isArray(row.limitRanges) && row.limitRanges.length ? row.limitRanges : [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }]; }, + addRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index + 1, 0, { lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }); }, + removeRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index, 1); if (!row.limitRanges.length) row.limitRanges.push({ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }); this.syncLegacyLimit(row); }, limitInput(row, index, prop, value) { row.limitRanges = this.getRanges(row); const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row.limitRanges[index][prop] = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; this.syncLegacyLimit(row); }, decimalInput(row, prop, value) { const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row[prop] = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; }, rangeUnitPriceInput(row, index, value) { row.limitRanges = this.getRanges(row); const target = { value }; this.decimalInput(target, 'value', value); row.limitRanges[index].unitPrice = target.value; if (index === 0) row.unitPrice = target.value; }, + rangeMinimumInput(row, index, value) { row.limitRanges = this.getRanges(row); const target = { value }; this.decimalInput(target, 'value', value); row.limitRanges[index].minimumBillingWeight = target.value; if (index === 0) row.minimumBillingWeight = target.value; }, addRule() { this.draft.rules.push(defaultRule()); }, copyRule(row) { const next = clone(row); this.draft.rules.push(this.normalizeRule(next)); this.loadFeeItems(next.feeType); }, removeRule(index) { this.draft.rules.splice(index, 1); if (!this.draft.rules.length) this.addRule(); }, validateRules() { const groups = {}; const feeItemSet = new Set(); const required = [['feeType', '费用类型'], ['feeItem', '费用项'], ['billingElement', '计费要素'], ['billingType', '计费类型'], ['billingUnit', '计费单位']]; for (const [i, row] of this.draft.rules.entries()) { const empty = required.find(([key]) => row[key] === undefined || row[key] === null || String(row[key]).trim() === ''); if (empty) { this.$message.warning(`第${i + 1}行${empty[1]}不能为空`); return false; } const feeItem = String(row.feeItem).trim(); if (feeItemSet.has(feeItem)) { this.$message.warning(`费用项“${feeItem}”不能重复`); return false; } feeItemSet.add(feeItem); if (!this.usesRangeUnitPrice(row) && (row.unitPrice === undefined || row.unitPrice === null || String(row.unitPrice).trim() === '')) { this.$message.warning(`第${i + 1}行单价不能为空`); return false; } if (!this.billingTypes(row).includes(row.billingType)) { this.$message.warning('请选择计费要素对应的计费类型'); return false; } if (!this.canEditLimit(row)) continue; const key = row.billingElement; groups[key] = groups[key] || []; for (const range of this.getRanges(row)) { const lower = Number(range.lowerLimit); const upper = Number(range.upperLimit); if (range.lowerLimit === '' || range.upperLimit === '' || Number.isNaN(lower) || Number.isNaN(upper)) { this.$message.warning('请完整填写计费要素上下限'); return false; } if (this.usesRangeUnitPrice(row) && (range.unitPrice === undefined || range.unitPrice === null || String(range.unitPrice).trim() === '')) { this.$message.warning('请为每组计费要素区间填写单价'); return false; } if (lower > upper) { this.$message.warning('计费要素下限不能大于上限'); return false; } groups[key].push({ lower, upper }); } } return this.validateRangeGroups(groups); }, validateRangeGroups(groups) { const precision = 0.000001; for (const [element, ranges] of Object.entries(groups)) { const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper); for (let i = 1; i < sorted.length; i += 1) { if (sorted[i].lower < sorted[i - 1].upper - precision) { this.$message.warning(`${element}的计费要素区间不能重叠`); return false; } if (sorted[i].lower > sorted[i - 1].upper + precision) { this.$message.warning(`${element}的计费要素区间必须连续,不能存在间隙`); return false; } } } return true; }, - save() { this.$refs.formRef.validate(valid => { if (!valid || !this.validateRules()) return; const plan = clone(this.draft); plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; return { ...rule, unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, limitRanges: ranges, lowerLimit: ranges[0]?.lowerLimit || '', upperLimit: ranges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? rule.minimumBillingWeight : '' }; }); this.$emit('save', plan, this.index); this.visible = false; }); }, + save() { this.$refs.formRef.validate(valid => { if (!valid || !this.validateRules()) return; const plan = clone(this.draft); plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; return { ...rule, unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, limitRanges: ranges, lowerLimit: ranges[0]?.lowerLimit || '', upperLimit: ranges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? (this.usesRangeMinimum(rule) ? ranges[0]?.minimumBillingWeight || '' : rule.minimumBillingWeight) : '' }; }); this.$emit('save', plan, this.index); this.visible = false; }); }, openMatch(row, index) { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); if (path.length) this.matchCargoChange(path); }); }, ensureRegions() { if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionRequest) return this.regionRequest; this.regionLoading = true; this.regionRequest = getRegionLazyTree().then(res => { const raw = res.data?.data || []; this.regionOptions = this.normalizeRegionTree(raw); return this.regionOptions; }).finally(() => { this.regionLoading = false; this.regionRequest = null; }); return this.regionRequest; }, normalizeRegionTree(list) { const map = new Map(); const flat = []; const walk = items => (items || []).forEach(item => { flat.push({ ...item, children: undefined }); if (item.children?.length) walk(item.children); }); walk(list); flat.forEach(item => map.set(String(item.id ?? item.code ?? item.value), { ...item, id: String(item.id ?? item.code ?? item.value), parentId: String(item.parentId ?? item.parentCode ?? ''), children: [] })); const roots = []; map.forEach(node => { const parent = map.get(node.parentId); if (parent && parent !== node) parent.children.push(node); else roots.push(node); }); const china = roots.find(item => ['中国', '中华人民共和国'].includes(item.title || item.name)); const source = china?.children?.length ? china.children : roots; const normalize = (node, level) => ({ ...node, title: node.title || node.name || '', leaf: level === 2, children: level === 1 ? (node.children || []).map(child => normalize(child, 2)) : undefined }); return source.map(node => normalize(node, 1)); }, diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index 4e90d4c..398df6c 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -230,6 +230,44 @@ + + + + + + + + + + + + 承运信息 - + visible && loadTaskCarrierOptions()" @change="handleTaskCarrierChange" > @@ -1221,14 +1263,18 @@ - + visible && loadTaskCarrierOptions()" @change="handleTaskCarrierChange" > @@ -4181,19 +4227,18 @@ - + visible && loadDispatchCarrierOptions()" @change="handleDispatchCarrierChange" > - + visible && loadDispatchCarrierOptions()" @change="handleDispatchCarrierChange" > { + const column = this.findColumn?.(this.option.column, prop); + if (column) column.disabled = disabled; + }); + }, + clearContractPartyOptions() { + this.contractPartyRequestId += 1; + this.contractPartyLoading = false; + this.contractPartyAOptions = []; + this.contractPartyBOptions = []; + }, + contractPartyNames(value, type = '') { + const names = []; + const append = item => { + if (item === undefined || item === null) return; + if (Array.isArray(item)) { + item.forEach(append); + return; + } + if (typeof item === 'object') { + append( + item[type === 'carrier' ? 'carrier' : 'customer'] || + item.fullName || + item.shortName || + item.customerName || + item.carrierName || + item.name + ); + return; + } + const text = String(item).trim(); + if (!text) return; + try { + const parsed = JSON.parse(text); + if (parsed !== item) { + append(parsed); + return; + } + } catch (error) { + // 普通名称字符串按项目页面使用的分隔符拆分。 + } + text + .split(/[、,,;;\n]/) + .map(name => name.trim()) + .filter(Boolean) + .forEach(name => { + if (!names.includes(name)) names.push(name); + }); + }; + append(value); + return names; + }, + contractPartyOptionsFromProject(project = {}, type = '') { + const names = this.contractPartyNames( + [ + project[type === 'carrier' ? 'carrierJson' : 'customerJson'], + project[type === 'carrier' ? 'carrierNames' : 'customerNames'], + project[type === 'carrier' ? 'carrierName' : 'customerName'], + project[type === 'carrier' ? 'carriers' : 'customers'], + ], + type + ); + return names.map(name => ({ label: name, value: name })); + }, + syncContractPartyOptions(project = {}, preserveCurrent = true) { + if (!this.isContractConfig) return; + this.contractPartyAOptions = this.contractPartyOptionsFromProject(project, 'customer'); + this.contractPartyBOptions = this.contractPartyOptionsFromProject(project, 'carrier'); + if (!preserveCurrent) { + this.form.partyA = ''; + this.form.partyB = ''; + } + }, + loadContractPartyOptions(projectId, project = {}, preserveCurrent = true) { + if (!this.isContractConfig || !projectId) { + this.clearContractPartyOptions(); + return Promise.resolve(); + } + const requestId = ++this.contractPartyRequestId; + this.syncContractPartyOptions(project, preserveCurrent); + this.contractPartyLoading = true; + return getProjectDetail(projectId) + .then(res => { + if (requestId !== this.contractPartyRequestId) return; + const detail = res?.data?.data || {}; + this.syncContractPartyOptions({ ...project, ...detail }, preserveCurrent); + }) + .catch(() => {}) + .finally(() => { + if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false; + }); + }, syncCustomCreateDialogButtons(type) { if ( !this.config.enableContractCreateActions && @@ -7868,6 +8030,24 @@ export default { } return true; }, + isDefaultBillingPlan(plan) { + const value = plan?.defaultPlan; + return value === true || value === 1 || ['true', '1'].includes(String(value).toLowerCase()); + }, + validateBillingPlanDefault() { + const plans = Array.isArray(this.billingPlanRows) ? this.billingPlanRows : []; + if (!plans.length) return true; + const defaultCount = plans.filter(plan => this.isDefaultBillingPlan(plan)).length; + if (defaultCount === 0) { + this.$message.warning('请勾选一个默认计费方案'); + return false; + } + if (defaultCount > 1) { + this.$message.warning('计费方案存在重复默认方案,请仅保留一个默认方案'); + return false; + } + return true; + }, validateSettlementRule(source = this.settlementRuleForm, label = '') { const rule = this.normalizeSettlementRule(source); this.settlementRuleErrors = {}; @@ -7944,6 +8124,13 @@ export default { submitRow.contractFileJson = JSON.stringify(this.contractFileRows); } if (this.config.enableBillingPlan) { + if ( + this.isContractConfig && + !skipValidation && + !this.validateBillingPlanDefault() + ) { + return false; + } submitRow.billingPlanJson = JSON.stringify(this.billingPlanRows); } if (this.config.enableSettlementRule) { @@ -8519,6 +8706,7 @@ export default { return; } if (this.isContractFormPage) { + this.$router.$avueRouter?.closeTag?.(); this.$router.push('/business/contract-manage'); return; } @@ -8575,12 +8763,15 @@ export default { this.syncCustomCreateDialogButtons(type); this.dialogReadonly = type === 'view'; this.syncContractNoDisabled(type); + this.syncContractPartyDisabled(); if (type === 'add') { this.form = { ...(this.config.defaultForm || {}), }; + this.syncContractPartyDisabled(); this.form.taskEntryMode = 'simple'; this.selectedProjectId = ''; + this.clearContractPartyOptions(); this.selectedOrganizationId = ''; this.attachmentRows = []; this.selectedAttachmentRows = []; @@ -8608,6 +8799,9 @@ export default { this.form.feeGenerationMode = this.form.feeGenerationMode || 'system'; this.changeRecordRows = []; this.applyCurrentUserHandler(); + this.taskCarrierRequestId += 1; + this.taskCarrierOptions = []; + this.taskCarrierLoading = false; this.initTaskInfoForm(); this.initTransportPlanFormRows(); this.syncContractPeriod(false); @@ -8644,6 +8838,14 @@ export default { this.suppressTransportTypeClear = false; }); this.selectedProjectId = this.form.projectId || ''; + this.syncContractPartyDisabled(); + this.loadContractPartyOptions(this.form.projectId, { + projectName: this.form.projectName, + customerNames: this.form.customerNames, + carrierNames: this.form.carrierNames, + customerJson: this.form.customerJson, + carrierJson: this.form.carrierJson, + }); this.loadProjectProcessConfigState(); this.selectedOrganizationId = this.form.organizationId || ''; this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson); @@ -8797,6 +8999,12 @@ export default { this.form.projectFundLimit = ''; this.form.usedFundLimit = 0; this.form.remainingFundLimit = 0; + if (this.isContractConfig) { + this.form.partyA = ''; + this.form.partyB = ''; + } + this.clearContractPartyOptions(); + this.syncContractPartyDisabled(); if (this.contractSelectEnabled) { this.form.contractId = ''; this.form.contractName = ''; @@ -8807,6 +9015,13 @@ export default { this.shippingPlanOptions = []; } this.hasProjectProcessConfig = false; + if (this.isWaybillDetailLayout) { + this.taskCarrierRequestId += 1; + this.taskCarrierOptions = []; + this.taskCarrierLoading = false; + this.form.carrierName = ''; + this.form.carrierId = ''; + } return; } this.form.projectId = project.id; @@ -8816,6 +9031,17 @@ export default { this.form.projectFundLimit = project.fundLimit || project.projectFundLimit || 0; this.form.usedFundLimit = 0; this.form.remainingFundLimit = this.calculateRemainingFundLimit(); + if (this.isWaybillDetailLayout) { + this.form.carrierName = ''; + this.form.carrierId = ''; + this.loadTaskCarrierOptions(project); + } + if (this.isContractConfig) { + this.form.partyA = ''; + this.form.partyB = ''; + this.loadContractPartyOptions(project.id, project, false); + } + this.syncContractPartyDisabled(); if (this.contractSelectEnabled || this.config.enableTransportPlanForm) { this.form.customerName = project.customerName || @@ -8852,6 +9078,7 @@ export default { return getContractList(1, 9999, { projectId: this.form.projectId, projectName: this.form.projectName, + ...(this.config.contractQueryParams || {}), }) .then(res => { this.contractOptions = extractRecords(res); @@ -9122,6 +9349,9 @@ export default { if (this.isTaskFullMode) { this.initTaskFullCargoRows(goodsRows); } + if (this.isWaybillDetailLayout && this.form.projectId) { + this.loadTaskCarrierOptions(); + } }, handleTaskEntryModeChange(value) { if ( @@ -9177,14 +9407,45 @@ export default { this.form.trailerVehicleNo = ''; this.form.escortName = ''; this.form.escortPhone = ''; - } else { + } else if (!this.isTaskCarrierRequired(nextValue)) { this.form.carrierName = ''; this.form.carrierId = ''; } this.form.carrierType = nextValue; }, - loadTaskCarrierOptions() { + isTaskCarrierRequired(carrierType) { + return ['承运商', '网货平台'].includes(String(carrierType || '').trim()); + }, + loadTaskCarrierOptions(project = {}) { if (!this.taskInfoFormEnabled || this.taskCarrierLoading) return Promise.resolve([]); + if (this.isWaybillDetailLayout) { + const projectId = project.id || project.projectId || this.form.projectId; + const requestId = ++this.taskCarrierRequestId; + const currentProject = this.projectOptions.find( + item => String(item.id) === String(projectId) + ); + this.taskCarrierOptions = this.getProjectCarrierOptions({ + ...currentProject, + ...project, + }); + if (!projectId) return Promise.resolve(this.taskCarrierOptions); + this.taskCarrierLoading = true; + return getProjectDetail(projectId) + .then(res => { + if (requestId !== this.taskCarrierRequestId) return this.taskCarrierOptions; + const detail = res?.data?.data || {}; + this.taskCarrierOptions = this.getProjectCarrierOptions({ + ...currentProject, + ...project, + ...detail, + }); + return this.taskCarrierOptions; + }) + .catch(() => this.taskCarrierOptions) + .finally(() => { + if (requestId === this.taskCarrierRequestId) this.taskCarrierLoading = false; + }); + } this.taskCarrierLoading = true; return getCarrierCustomerList(1, 9999, { customerType: '承运商', status: 1 }) .then(res => { @@ -9540,8 +9801,8 @@ export default { ['carrierType', '承运类型'], ['vehicleNo', this.transportVehicleNoLabel], ]; - if (row.carrierType === '承运商') { - requiredFields.push(['carrierName', '承运商']); + if (this.isTaskCarrierRequired(row.carrierType)) { + requiredFields.push(['carrierName', row.carrierType === '网货平台' ? '承运方' : '承运商']); } if (this.isRoadTransport && row.carrierType === '自运') { requiredFields.push(['driverName', '司机']); @@ -11890,7 +12151,11 @@ export default { this.transportPlanImportContractOptions = []; if (!projectId) return; this.transportPlanImportContractLoading = true; - getContractList(1, 9999, { projectId, projectName: project?.projectName }) + getContractList(1, 9999, { + projectId, + projectName: project?.projectName, + ...(this.config.contractQueryParams || {}), + }) .then(res => { this.transportPlanImportContractOptions = extractRecords(res); const contract = this.transportPlanImportContractOptions[0]; @@ -12264,6 +12529,8 @@ export default { return; } this.dispatchRow = { ...row }; + this.dispatchCarrierOptions = []; + this.dispatchCarrierRequestId += 1; this.dispatchBox = true; this.dispatchLoading = true; this.dispatchRows = []; @@ -12280,6 +12547,7 @@ export default { }; this.dispatchRow = detail; this.dispatchRows = this.buildDispatchRows(detail); + this.loadDispatchCarrierOptions(detail); }) .finally(() => { this.dispatchLoading = false; @@ -12313,6 +12581,9 @@ export default { this.dispatchRows = []; this.dispatchLoading = false; this.dispatchSaving = ''; + this.dispatchCarrierRequestId += 1; + this.dispatchCarrierOptions = []; + this.dispatchCarrierLoading = false; this.dispatchItemBox = false; this.dispatchItemIndex = -1; this.dispatchItemForm = defaultDispatchRow(); @@ -12911,30 +13182,72 @@ export default { this.dispatchItemForm.trailerVehicleNo = ''; this.dispatchItemForm.escortName = ''; this.dispatchItemForm.escortPhone = ''; - } else { + } else if (!this.isDispatchCarrierRequired(nextValue)) { this.dispatchItemForm.carrierId = ''; this.dispatchItemForm.carrierName = ''; } this.dispatchItemForm.carrierType = nextValue; }, - loadDispatchCarrierOptions() { - if (this.taskCarrierLoading) return Promise.resolve([]); - this.taskCarrierLoading = true; - return getCarrierCustomerList(1, 9999, { customerType: '承运商', status: 1 }) + isDispatchCarrierRequired(carrierType) { + return ['承运商', '网货平台'].includes(String(carrierType || '').trim()); + }, + getProjectCarrierOptions(project = {}) { + const rows = this.parseJsonArray(project.carrierJson); + const names = String(project.carrierNames || '') + .split(/[、,,;;\n]/) + .map(item => item.trim()) + .filter(Boolean); + const options = []; + const append = item => { + if (!item) return; + const id = item.id || item.carrierId || item.customerId || ''; + const label = + (typeof item === 'string' ? item : '') || + item.carrier || + item.carrierName || + item.fullName || + item.shortName || + item.customerName || + item.name || + ''; + const value = String(label).trim(); + if (!value || options.some(option => option.value === value)) return; + options.push({ id, carrierName: value, fullName: value, value }); + }; + rows.forEach(append); + names.forEach(append); + return options; + }, + loadDispatchCarrierOptions(plan = this.dispatchRow) { + if (this.dispatchCarrierLoading) return Promise.resolve(this.dispatchCarrierOptions); + const projectId = plan?.projectId; + const requestId = ++this.dispatchCarrierRequestId; + const project = this.projectOptions.find( + item => String(item.id) === String(projectId) + ); + this.dispatchCarrierOptions = this.getProjectCarrierOptions({ ...project, ...plan }); + if (!projectId) return Promise.resolve(this.dispatchCarrierOptions); + this.dispatchCarrierLoading = true; + return getProjectDetail(projectId) .then(res => { - this.taskCarrierOptions = extractRecords(res); - return this.taskCarrierOptions; + if (requestId !== this.dispatchCarrierRequestId) return this.dispatchCarrierOptions; + const detail = res?.data?.data || {}; + this.dispatchCarrierOptions = this.getProjectCarrierOptions({ + ...project, + ...plan, + ...detail, + }); + return this.dispatchCarrierOptions; }) + .catch(() => this.dispatchCarrierOptions) .finally(() => { - this.taskCarrierLoading = false; + if (requestId === this.dispatchCarrierRequestId) { + this.dispatchCarrierLoading = false; + } }); }, handleDispatchCarrierChange(value) { - const carrier = this.taskCarrierOptions.find(item => - [item.customerName, item.carrierName, item.fullName, item.name].some( - name => String(name || '') === String(value || '') - ) - ); + const carrier = this.dispatchCarrierOptions.find(item => item.value === String(value || '')); this.dispatchItemForm.carrierId = carrier?.id || ''; this.dispatchItemForm.carrierName = value || ''; }, @@ -13140,8 +13453,8 @@ export default { : '车牌号', ], ]; - if (row.carrierType === '承运商') { - requiredFields.push(['carrierName', '承运商']); + if (this.isDispatchCarrierRequired(row.carrierType)) { + requiredFields.push(['carrierName', row.carrierType === '网货平台' ? '承运方' : '承运商']); } const fullEntry = row.taskEntryMode === 'full'; const roadTransport = this.resolveTransportMode(row.transportType) === 'road'; diff --git a/src/views/settlement/formal-settlement.vue b/src/views/settlement/formal-settlement.vue index 67f86be..dd0cee5 100644 --- a/src/views/settlement/formal-settlement.vue +++ b/src/views/settlement/formal-settlement.vue @@ -57,6 +57,7 @@ v-if="hasPermission('formal_settlement_payment')" type="primary" plain + disabled @click="openPaymentDialog" >付款申请 @@ -64,6 +65,7 @@ v-if="hasPermission('formal_settlement_sync')" type="primary" plain + disabled @click="handleSync" >同步金蝶 diff --git a/src/views/settlement/pre-settlement.vue b/src/views/settlement/pre-settlement.vue index 050c405..bcffd8b 100644 --- a/src/views/settlement/pre-settlement.vue +++ b/src/views/settlement/pre-settlement.vue @@ -53,6 +53,7 @@ v-if="hasPermission('pre_settlement_advance')" type="primary" plain + disabled @click="openAdvanceDialog" > 预付申请 @@ -61,6 +62,7 @@ v-if="hasPermission('pre_settlement_formal')" type="primary" plain + disabled @click="handleFormalSettlement" > 尾款结算 diff --git a/src/views/settlement/receivable-payable-detail.vue b/src/views/settlement/receivable-payable-detail.vue index 8789d04..c1d3369 100644 --- a/src/views/settlement/receivable-payable-detail.vue +++ b/src/views/settlement/receivable-payable-detail.vue @@ -680,7 +680,12 @@ export default { } }, async loadContracts() { - const res = await getContractList(1, 999, { approvalStatus: 'approved' }); + const contractCategory = + this.settlementType === 'payable' ? '承运商合同' : '客户合同'; + const res = await getContractList(1, 999, { + approvalStatus: 'approved', + contractCategory, + }); const data = this.unwrapPage(res); this.contractOptions = data.records || []; }, diff --git a/src/views/settlement/transport-reconciliation.vue b/src/views/settlement/transport-reconciliation.vue index b38d72e..dce8265 100644 --- a/src/views/settlement/transport-reconciliation.vue +++ b/src/views/settlement/transport-reconciliation.vue @@ -31,10 +31,6 @@ - - - - + + + + @@ -314,6 +319,12 @@ export default { align-items: center; gap: 8px; } +.reconciliation-page__settlement-tabs { + margin-bottom: 0; +} +.reconciliation-page__settlement-tabs :deep(.el-tabs__header) { + margin: 0; +} .reconciliation-page__links { display: flex; flex-wrap: wrap;