diff --git a/src/option/business/common.js b/src/option/business/common.js index 26b5cea..1f6feb2 100644 --- a/src/option/business/common.js +++ b/src/option/business/common.js @@ -130,12 +130,18 @@ export const contractApprovalStatusOptions = [ { label: '草稿', value: 'draft' }, { label: '审批中', value: 'reviewing' }, { label: '已驳回', value: 'rejected' }, + { label: '已撤回', value: 'withdrawn' }, { label: '审批通过', value: 'approved' }, { label: '变更审批中', value: 'change_reviewing' }, { label: '变更驳回', value: 'change_rejected' }, { label: '变更审批通过', value: 'change_approved' }, ]; +export const contractArchiveStatusOptions = [ + { label: '未归档', value: '未归档' }, + { label: '已归档', value: '已归档' }, +]; + export const nodeOptions = [ { label: '接单', value: '接单' }, { label: '到场', value: '到场' }, diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index 06342c0..86f25dd 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -1,6 +1,7 @@ import { auditColumns, contractApprovalStatusOptions, + contractArchiveStatusOptions, contractCategoryOptions, contractStageOptions, createCrudOption, @@ -48,7 +49,7 @@ export const config = { batchDelete: false, enableProjectSelect: true, projectQueryParams: { - approvalStatuses: 'approved,change_approved', + contractSelectable: true, }, actions: ['copy'], statusProp: 'approvalStatus', @@ -71,6 +72,10 @@ export const config = { ['legalSealFlag', '是否需要加盖法人章'], ['copyCount', '一式(份)'], ['paymentDays', '回款账期(天)'], + ['contractAmount', '合同金额'], + ['templateFlag', '是否范本'], + ['originalContractNo', '原件合同编号'], + ['electronicSealFlag', '是否电子章'], ['remark', '备注', 2], ], }, @@ -84,6 +89,7 @@ export const config = { ['effectiveType', '生效类型'], ['contractStage', '合同阶段'], ['approvalStatus', '审核状态'], + ['archiveStatus', '归档状态'], ['currentNode', '当前节点'], ['currentProcessor', '当前处理人'], ], @@ -104,7 +110,7 @@ export const config = { }, deleteStatus: ['draft'], deleteStage: ['draft'], - editStatus: ['draft', 'rejected', 'change_rejected'], + editStatus: ['draft', 'withdrawn', 'rejected', 'change_rejected'], operations: [ { action: 'flow', @@ -271,7 +277,7 @@ export const option = createCrudOption([ formslot: true, order: 165, dicUrl: - '/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved', + '/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true', dicFormatter: projectNameDicFormatter, props: { label: 'projectName', @@ -485,6 +491,47 @@ export const option = createCrudOption([ order: 50, minWidth: 130, }, + { + label: '合同金额', + prop: 'contractAmount', + type: 'number', + min: 0, + precision: 2, + hide: true, + order: 45, + minWidth: 130, + }, + { + label: '是否范本', + prop: 'templateFlag', + type: 'select', + dicData: [ + { label: '否', value: 0 }, + { label: '是', value: 1 }, + ], + hide: true, + order: 40, + minWidth: 100, + }, + { + label: '原件合同编号', + prop: 'originalContractNo', + hide: true, + order: 35, + minWidth: 160, + }, + { + label: '是否电子章', + prop: 'electronicSealFlag', + type: 'select', + dicData: [ + { label: '否', value: 0 }, + { label: '是', value: 1 }, + ], + hide: true, + order: 30, + minWidth: 110, + }, { label: '创建时间', prop: 'createTime', @@ -522,6 +569,19 @@ export const option = createCrudOption([ addDisplay: false, editDisplay: false, }, + { + label: '归档状态', + prop: 'archiveStatus', + type: 'select', + search: true, + searchOrder: 5, + searchPlaceholder: '请选择', + slot: true, + dicData: contractArchiveStatusOptions, + minWidth: 110, + addDisplay: false, + editDisplay: false, + }, { label: '当前节点', prop: 'currentNode', diff --git a/src/views/business/components/billing-plan-editor.vue b/src/views/business/components/billing-plan-editor.vue index 00fae7c..2984093 100644 --- a/src/views/business/components/billing-plan-editor.vue +++ b/src/views/business/components/billing-plan-editor.vue @@ -229,32 +229,38 @@ - @@ -297,9 +303,6 @@ {{ displayValue(matchForm.destination) }} - {{ - transportModeLabel(matchForm.transportMode) - }} {{ displayValue(matchForm.cargoType) }} @@ -334,21 +337,6 @@ @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('destination', v)" /> - ({ - 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 !== '' - ) - : []; + const rawRanges = Array.isArray(row.limitRanges) ? row.limitRanges : []; + const ranges = rawRanges + .map((item, index) => ({ + lowerLimit: item?.lowerLimit ?? '', + upperLimit: item?.upperLimit ?? '', + unitPrice: + item?.unitPrice === undefined || item?.unitPrice === '' + ? row.unitPrice ?? '' + : item.unitPrice, + minimumBillingWeight: this.resolveRangeMinimum(row, item, index), + })) + .filter( + item => + item.lowerLimit !== '' || + item.upperLimit !== '' || + item.unitPrice !== '' || + item.minimumBillingWeight !== '' + ); if ( !ranges.length && (row.lowerLimit !== '' || @@ -598,9 +580,23 @@ export default { minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '', }); return ranges.length - ? ranges + ? ranges.map((item, index) => ({ + ...item, + minimumBillingWeight: this.usesRangeMinimum(row) + ? index === 0 + ? item.minimumBillingWeight + : '' + : '', + })) : [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }]; }, + resolveRangeMinimum(row, item, index) { + if (!this.usesRangeMinimum(row) || index !== 0) return ''; + if (item?.minimumBillingWeight !== undefined && item?.minimumBillingWeight !== '') { + return item.minimumBillingWeight; + } + return row.minimumBillingWeight ?? ''; + }, syncLegacyLimit(row) { const first = row.limitRanges?.[0] || {}; row.lowerLimit = first.lowerLimit ?? ''; @@ -803,11 +799,16 @@ export default { if (index === 0) row.unitPrice = target.value; }, rangeMinimumInput(row, index, value) { + if (index !== 0) return; 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; + row.limitRanges[0].minimumBillingWeight = target.value; + row.minimumBillingWeight = target.value; + row.limitRanges = row.limitRanges.map((item, rangeIndex) => ({ + ...item, + minimumBillingWeight: rangeIndex === 0 ? target.value : '', + })); }, addRule() { this.draft.rules.push(defaultRule()); @@ -929,15 +930,22 @@ export default { plan.transportModeLabel = transportMode?.label || plan.transportMode; plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; + const normalizedRanges = ranges.map((item, rangeIndex) => ({ + ...item, + minimumBillingWeight: + this.usesRangeMinimum(rule) && rangeIndex === 0 ? item.minimumBillingWeight || '' : '', + })); return { ...rule, - unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, - limitRanges: ranges, - lowerLimit: ranges[0]?.lowerLimit || '', - upperLimit: ranges[0]?.upperLimit || '', + unitPrice: this.usesRangeUnitPrice(rule) + ? normalizedRanges[0]?.unitPrice || '' + : rule.unitPrice, + limitRanges: normalizedRanges, + lowerLimit: normalizedRanges[0]?.lowerLimit || '', + upperLimit: normalizedRanges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? this.usesRangeMinimum(rule) - ? ranges[0]?.minimumBillingWeight || '' + ? normalizedRanges[0]?.minimumBillingWeight || '' : rule.minimumBillingWeight : '', }; @@ -950,9 +958,6 @@ export default { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; - this.ensureTransportModes().then(() => { - this.matchForm.transportMode = this.normalizeTransportMode(this.matchForm.transportMode); - }); this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); @@ -977,21 +982,6 @@ export default { }); return this.transportModeRequest; }, - normalizeTransportMode(value) { - const transportMode = String(value || '').trim(); - if (!transportMode) return ''; - const option = this.transportModeOptions.find(item => { - const optionValue = String(item.value || '').trim(); - const optionLabel = String(item.label || '').trim(); - return ( - optionValue === transportMode || - optionLabel === transportMode || - optionLabel.includes(transportMode) || - transportMode.includes(optionLabel) - ); - }); - return option?.value || transportMode; - }, ensureRegions() { if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionRequest) return this.regionRequest; @@ -1214,6 +1204,11 @@ export default { cursor: pointer; } +.billing-plan-editor__column-title { + display: inline-flex; + align-items: center; +} + .limit-list { display: flex; flex-direction: column; @@ -1229,4 +1224,8 @@ export default { .limit-row .el-input { flex: 1; } + +.limit-placeholder { + height: 32px; +} diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index 419eb99..0e4afc7 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -1927,13 +1927,13 @@ @@ -1986,6 +1986,59 @@ +
+ + + + + + + + + + + + + +
@@ -5412,13 +5465,19 @@ const defaultBillingPlan = () => ({ }); const defaultSettlementRule = () => ({ - autoGenerate: 1, + autoGenerate: '', billStartDate: '', - settlementType: '月结', - billCycleType: '固定截单日', - billCutoffDay: 25, + settlementType: '', + billCycleType: '', + billCutoffDay: '', cycleDays: '', + customPeriods: [], }); +const normalizeCustomPeriods = (periods = []) => + (periods || []).map(item => ({ + startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '', + endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '', + })); const defaultReconciliation = () => ({ skipReconciliation: 1, @@ -5837,7 +5896,7 @@ export default { 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], }, settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'], - billCycleTypeOptions: ['固定截单日', '自然月'], + billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'], reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'], packageOptions, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'], @@ -6448,6 +6507,11 @@ export default { this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日' ); }, + showSettlementCustomPeriods() { + return ( + this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期' + ); + }, showSettlementCycleDays() { return this.settlementRuleForm.settlementType === '固定天数周期结算'; }, @@ -7025,7 +7089,7 @@ export default { if (!row) return '-'; if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'feeGenerationMode') { - return row[prop] === 'manual' ? '手动生成' : '系统生成'; + return row[prop] === 'manual' ? '账单导入生成' : '系统生成'; } const column = this.findColumn(this.option.column, prop); if (column?.formatter) return column.formatter(row, {}, row[prop]); @@ -7566,23 +7630,32 @@ export default { isFixedBillCutoffSettlement(rule = this.settlementRuleForm) { return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日'; }, + isCustomMultiPeriodSettlement(rule = this.settlementRuleForm) { + return this.isMonthlySettlement(rule) && rule.billCycleType === '自定义多周期'; + }, isFixedCycleSettlement(rule = this.settlementRuleForm) { return rule.settlementType === '固定天数周期结算'; }, handleSettlementTypeChange(value) { this.clearSettlementRuleError('settlementType'); if (value === '月结') { - if (!this.settlementRuleForm.billCycleType) { - this.settlementRuleForm.billCycleType = '固定截单日'; - } if (this.settlementRuleForm.billCycleType === '固定截单日') { - this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; + this.settlementRuleForm.customPeriods = []; + } else if (this.settlementRuleForm.billCycleType === '自定义多周期') { + this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = normalizeCustomPeriods( + this.settlementRuleForm.customPeriods || [] + ); + } else { + this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = []; } this.settlementRuleForm.cycleDays = ''; return; } this.settlementRuleForm.billCycleType = ''; this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = []; if (value !== '固定天数周期结算') { this.settlementRuleForm.cycleDays = ''; } @@ -7590,10 +7663,17 @@ export default { handleSettlementBillCycleTypeChange(value) { this.clearSettlementRuleError('billCycleType'); if (value === '固定截单日') { - this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; + this.settlementRuleForm.customPeriods = []; return; } this.settlementRuleForm.billCutoffDay = ''; + if (value === '自定义多周期') { + this.settlementRuleForm.customPeriods = normalizeCustomPeriods( + this.settlementRuleForm.customPeriods || [] + ); + return; + } + this.settlementRuleForm.customPeriods = []; }, normalizeSettlementRule(rule = this.settlementRuleForm) { const nextRule = { @@ -7603,14 +7683,94 @@ export default { if (!this.isMonthlySettlement(nextRule)) { nextRule.billCycleType = ''; nextRule.billCutoffDay = ''; + nextRule.customPeriods = []; } else if (!this.isFixedBillCutoffSettlement(nextRule)) { nextRule.billCutoffDay = ''; } + if (this.isCustomMultiPeriodSettlement(nextRule)) { + nextRule.customPeriods = normalizeCustomPeriods( + Array.isArray(nextRule.customPeriods) ? nextRule.customPeriods : [] + ); + } else { + nextRule.customPeriods = []; + } if (!this.isFixedCycleSettlement(nextRule)) { nextRule.cycleDays = ''; } return nextRule; }, + customPeriodEndDayOptions(row) { + const startDay = Number(row?.startDay); + if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; + return Array.from({ length: 31 - startDay + 1 }, (_, index) => { + const value = startDay + index; + return { label: `${value}日`, value }; + }); + }, + handleCustomPeriodStartDayChange(index, value) { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + const row = periods[index]; + if (!row) return; + row.startDay = value || ''; + const startDay = Number(row.startDay); + const endDay = Number(row.endDay); + if ( + Number.isFinite(startDay) && + Number.isFinite(endDay) && + (endDay < startDay || endDay > 31) + ) { + row.endDay = ''; + } + this.settlementRuleForm.customPeriods = periods; + }, + handleCustomPeriodEndDayChange(index, value) { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + const row = periods[index]; + if (!row) return; + row.endDay = value || ''; + this.settlementRuleForm.customPeriods = periods; + }, + addCustomPeriodRow() { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + periods.push({ startDay: '', endDay: '' }); + this.settlementRuleForm.customPeriods = periods; + }, + removeCustomPeriodRow(index) { + if (index <= 0) return; + const periods = [...(this.settlementRuleForm.customPeriods || [])]; + periods.splice(index, 1); + this.settlementRuleForm.customPeriods = normalizeCustomPeriods(periods); + }, + validateCustomPeriods(periods = [], label = '') { + const rows = normalizeCustomPeriods(periods); + const prefix = label ? `${label}:` : ''; + if (!rows.length) { + this.$message.warning(`${prefix}请至少配置一段自定义周期`); + return false; + } + for (let index = 0; index < rows.length; index += 1) { + const row = rows[index]; + const startDay = Number(row.startDay); + const endDay = Number(row.endDay); + if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { + this.$message.warning(`${prefix}请选择第${index + 1}行运单区间开始日`); + return false; + } + if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { + this.$message.warning(`${prefix}请选择第${index + 1}行运单区间结束日`); + return false; + } + if (endDay < startDay) { + this.$message.warning(`${prefix}第${index + 1}行结束日不能早于开始日`); + return false; + } + if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { + this.$message.warning(`${prefix}自定义多周期区间必须连续,不允许重叠或存在日期缺口`); + return false; + } + } + return true; + }, clearSettlementRuleError(field) { if (!this.settlementRuleErrors[field]) return; this.settlementRuleErrors = { @@ -7632,8 +7792,8 @@ export default { return false; } if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) { - this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' }; - this.$message.warning(`${label}${label ? ':' : ''}请选择账单周期类型`); + this.settlementRuleErrors = { billCycleType: '请选择结算周期' }; + this.$message.warning(`${label}${label ? ':' : ''}请选择结算周期`); return false; } if (this.isFixedBillCutoffSettlement(rule)) { @@ -7649,6 +7809,9 @@ export default { return false; } } + if (this.isCustomMultiPeriodSettlement(rule)) { + return this.validateCustomPeriods(rule.customPeriods, label); + } if (this.isFixedCycleSettlement(rule)) { const cycleDays = Number(rule.cycleDays); if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) { @@ -11239,8 +11402,10 @@ export default { if (!this.canEditBillingLimit(row)) { continue; } - const key = row.billingElement || ''; - groups[key] = groups[key] || []; + const feeItem = String(row.feeItem || '').trim(); + const billingElement = String(row.billingElement || '').trim(); + const key = JSON.stringify([feeItem, billingElement]); + groups[key] = groups[key] || { feeItem, billingElement, ranges: [] }; const ranges = this.getBillingLimitRanges(row); for (const range of ranges) { const lower = Number(range.lowerLimit); @@ -11258,14 +11423,14 @@ export default { this.$message.warning('计费要素下限不能大于上限'); return false; } - groups[key].push({ lower, upper }); + groups[key].ranges.push({ lower, upper }); } } return this.validateBillingLimitRangeGroups(groups); }, validateBillingLimitRangeGroups(groups) { const precision = 0.000001; - for (const [billingElement, ranges] of Object.entries(groups)) { + for (const { feeItem, billingElement, ranges } of Object.values(groups)) { if (ranges.length <= 1) continue; const sortedRanges = [...ranges].sort((prev, next) => { if (prev.lower !== next.lower) return prev.lower - next.lower; @@ -11275,11 +11440,13 @@ export default { const prevRange = sortedRanges[index - 1]; const currentRange = sortedRanges[index]; if (currentRange.lower < prevRange.upper - precision) { - this.$message.warning(`${billingElement}的计费要素区间不能重叠`); + this.$message.warning(`费用项“${feeItem}”${billingElement}的计费要素区间不能重叠`); return false; } if (currentRange.lower > prevRange.upper + precision) { - this.$message.warning(`${billingElement}的计费要素区间必须连续,不能存在间隙`); + this.$message.warning( + `费用项“${feeItem}”${billingElement}的计费要素区间必须连续,不能存在间隙` + ); return false; } } diff --git a/src/views/business/components/contract-attachment-section.vue b/src/views/business/components/contract-attachment-section.vue new file mode 100644 index 0000000..fdf4225 --- /dev/null +++ b/src/views/business/components/contract-attachment-section.vue @@ -0,0 +1,493 @@ + + + + + diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 5a6d7ab..2dc76b5 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -4287,7 +4287,7 @@ export default { if (!row) return '-'; if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'feeGenerationMode') { - return row[prop] === 'manual' ? '手动生成' : '系统生成'; + return row[prop] === 'manual' ? '账单导入生成' : '系统生成'; } const column = this.findColumn(this.option.column, prop); if (column?.formatter) return column.formatter(row, {}, row[prop]); diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index c654141..71071ff 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -25,6 +25,10 @@ + + + + @@ -42,7 +46,7 @@
计费信息
添加
- 系统生成手动生成 + 系统生成账单导入生成
@@ -65,10 +69,17 @@ - + + + + + + + +
@@ -144,15 +155,27 @@ const normalizeOptionalPositiveInteger = value => { const number = Number(value); return Number.isInteger(number) && number > 0 ? number : null; }; +const normalizeOptionalAmount = value => { + if (value === undefined || value === null || value === '') return null; + const number = Number(value); + return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null; +}; + + +const normalizeCustomPeriods = (periods = []) => + (periods || []).map(item => ({ + startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '', + endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '', + })); export default { components: { BillingPlanEditor, ElImageViewer, OpenFileViewer }, - data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; }, - computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } }, + data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [] }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月','自定义多周期'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; }, + computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCustomPeriods() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '自定义多周期'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } }, mounted() { this.load(); }, watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } }, methods: { - async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, + async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeContent: '', changeReason: '', changeAttachmentsJson: '', copyCount: normalizeOptionalPositiveInteger(data.copyCount), paymentDays: normalizeOptionalPositiveInteger(data.paymentDays), contractAmount: normalizeOptionalAmount(data.contractAmount), templateFlag: data.templateFlag ?? 0, electronicSealFlag: data.electronicSealFlag ?? 0, changeType: '合同信息变更' }; this.changeMaterials = []; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.mergeChangeAttachments(this.parse(data.attachmentsJson), this.parse(data.changeAttachmentsJson)); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; const normalizeRule = rule => { const next = { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [], ...(rule || {}) }; if (next.settlementType === '月结' && next.billCycleType === '自定义多周期') next.customPeriods = normalizeCustomPeriods(Array.isArray(next.customPeriods) ? next.customPeriods : []); else next.customPeriods = []; return next; }; this.preSettlementConfig = normalizeRule(rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy)); this.formalSettlementConfig = normalizeRule(rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy)); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); }, parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } }, // 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示 mergeChangeAttachments(attachments = [], changeAttachments = []) { @@ -163,14 +186,14 @@ export default { .map(item => ({ ...item, size: /^\d+$/.test(String(item.size ?? '')) ? this.formatFileSize(item.size) : item.size })); return [...attachments, ...merged]; }, - parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } }, + parseObject(value) { try { return { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [], ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: '', billStartDate: '', settlementType: '', billCycleType: '', billCutoffDay: '', cycleDays: '', customPeriods: [] }; } }, positiveIntegerInput(prop, value) { this.form[prop] = String(value ?? '').replace(/\D/g, '').replace(/^0+/, ''); }, addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; }, editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; }, toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); }, savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); }, - handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; }, - handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; }, + handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; this.settlementRule.customPeriods = []; } else if (this.settlementRule.billCycleType === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; }, + handleCycleTypeChange(value) { if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; if (value === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; }, currentUploadUserName() { const userInfo = this.$store.getters.userInfo || {}; return userInfo.realName || userInfo.userName || ''; @@ -209,7 +232,14 @@ export default { handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); }, formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; }, addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); }, - async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); }, + customPeriodEndDayOptions(row) { const startDay = Number(row?.startDay); if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; return Array.from({ length: 31 - startDay + 1 }, (_, index) => ({ label: `${startDay + index}日`, value: startDay + index })); }, + handleCustomPeriodStartDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.startDay = value || ''; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (Number.isFinite(startDay) && Number.isFinite(endDay) && (endDay < startDay || endDay > 31)) row.endDay = ''; this.settlementRule.customPeriods = periods; }, + handleCustomPeriodEndDayChange(index, value) { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); const row = periods[index]; if (!row) return; row.endDay = value || ''; this.settlementRule.customPeriods = periods; }, + addCustomPeriodRow() { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); periods.push({ startDay: '', endDay: '' }); this.settlementRule.customPeriods = periods; }, + removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); }, + validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; }, + validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日期和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; }, + async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); }, }, }; diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index c8fc9fd..6d0f968 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -64,6 +64,12 @@ + +