From 80a15fc6262829f635057c95627ab4a6de8d137d Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 1 Sep 2026 17:35:53 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E8=B0=83=E6=95=B4=E6=94=B6=E4=BB=98?= =?UTF-8?q?=E6=AC=BE=202=E3=80=81=E8=B0=83=E6=95=B4=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=203=E3=80=81=E8=B0=83=E6=95=B4=E7=BB=93?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/settlement/preSettlement.js | 12 + .../settlement/receivable-payable-detail.js | 7 + src/views/base/fee-item.vue | 68 ++- src/views/base/port-terminal.vue | 4 +- src/views/base/railway-station.vue | 18 +- .../components/pre-settlement-editor.vue | 392 +++++++++++-- .../settlement/receivable-payable-detail.vue | 552 ++++++++++++++++-- 7 files changed, 932 insertions(+), 121 deletions(-) diff --git a/src/api/settlement/preSettlement.js b/src/api/settlement/preSettlement.js index 2b96d13..bb61417 100644 --- a/src/api/settlement/preSettlement.js +++ b/src/api/settlement/preSettlement.js @@ -10,6 +10,18 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get' export const getContractOptions = keyword => request({ url: `${baseUrl}/contract-options`, method: 'get', params: { keyword } }); +export const getContractList = (current, size, params) => + request({ + url: '/blade-transport/contract-manage/list', + method: 'get', + params: { + current, + size, + approvalStatuses: 'approved,change_approved', + ...params, + }, + }); + export const getFeeOptions = () => request({ url: `${baseUrl}/fee-options`, method: 'get' }); export const getCandidateDetails = (current, size, params) => diff --git a/src/option/settlement/receivable-payable-detail.js b/src/option/settlement/receivable-payable-detail.js index dd91680..745ca2e 100644 --- a/src/option/settlement/receivable-payable-detail.js +++ b/src/option/settlement/receivable-payable-detail.js @@ -138,4 +138,11 @@ export const generatePreviewColumns = [ { label: '货物名称', prop: 'cargoName', minWidth: 140 }, { label: '货物类型', prop: 'cargoType', minWidth: 140 }, { label: '规格', prop: 'specification', minWidth: 140 }, + { label: '型号', prop: 'model', minWidth: 120 }, + { label: '计费要素', prop: 'billingFactor', minWidth: 130 }, + { label: '计费类型', prop: 'billingType', minWidth: 130 }, + { label: '运输量', prop: 'transportQuantityText', minWidth: 120 }, + { label: '运费计算单位', prop: 'priceUnit', minWidth: 130 }, + { label: '运输单价', prop: 'unitPrice', minWidth: 120 }, + { label: '里程(KM)', prop: 'mileage', minWidth: 120 }, ]; diff --git a/src/views/base/fee-item.vue b/src/views/base/fee-item.vue index de2b752..fd56038 100644 --- a/src/views/base/fee-item.vue +++ b/src/views/base/fee-item.vue @@ -35,7 +35,6 @@ v-model="form.feeCategory" class="fee-item-form-control" clearable - :disabled="dialogType !== 'add'" filterable placeholder="请选择费用类型" @change="handleFeeCategoryChange" @@ -43,7 +42,7 @@ @@ -54,7 +53,6 @@ :maxlength="feeItemCodeMaxlength" class="fee-item-form-control" clearable - :disabled="dialogType !== 'add'" placeholder="请输入费用项代码" @change="handleFeeItemCodeChange" > @@ -339,31 +337,63 @@ export default { values.englishName = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory); return values; }, + validateUnique(row) { + const rowId = String(row.id || ''); + const hasDuplicate = (params, prop) => { + if (!row[prop]) { + return Promise.resolve(false); + } + return getList(1, 100, params).then(res => { + const records = res.data.data.records || []; + return records.some( + item => + String(item[prop] || '').trim() === String(row[prop]).trim() && + String(item.id || '') !== rowId + ); + }); + }; + return Promise.all([ + hasDuplicate({ name: row.name }, 'name'), + hasDuplicate({ englishName: row.englishName }, 'englishName'), + ]).then(([nameExists, codeExists]) => { + if (nameExists) { + return Promise.reject(new Error('该费用项已存在')); + } + if (codeExists) { + return Promise.reject(new Error('该费用项代码已存在')); + } + return Promise.resolve(); + }); + }, + handleSubmitError(error, loading) { + const uniqueMessages = ['该费用项已存在', '该费用项代码已存在']; + if (uniqueMessages.includes(error.message)) { + this.$message.warning(error.message); + } + window.console.log(error); + loading(); + }, rowSave(row, done, loading) { - submit(this.normalizeRow(row)).then( - () => { + const submitRow = this.normalizeRow(row); + this.validateUnique(submitRow) + .then(() => submit(submitRow)) + .then(() => { this.onLoad(this.page); this.$message({ type: 'success', message: '操作成功!' }); done(); - }, - error => { - window.console.log(error); - loading(); - } - ); + }) + .catch(error => this.handleSubmitError(error, loading)); }, rowUpdate(row, index, done, loading) { - submit(this.normalizeRow(row)).then( - () => { + const submitRow = this.normalizeRow(row); + this.validateUnique(submitRow) + .then(() => submit(submitRow)) + .then(() => { this.onLoad(this.page); this.$message({ type: 'success', message: '操作成功!' }); done(); - }, - error => { - window.console.log(error); - loading(); - } - ); + }) + .catch(error => this.handleSubmitError(error, loading)); }, rowDel(row) { this.$confirm('确定将选择数据删除?', { diff --git a/src/views/base/port-terminal.vue b/src/views/base/port-terminal.vue index 79589de..94ed862 100644 --- a/src/views/base/port-terminal.vue +++ b/src/views/base/port-terminal.vue @@ -110,7 +110,9 @@ maxlength="24" placeholder="请输入码头标识" @input="handleTerminalCodeChange" - /> + > + + @@ -698,6 +890,28 @@ import { exportBlob } from '@/api/common'; import { getDictionary } from '@/api/system/dictbiz'; import { downloadXls } from '@/utils/util'; import { createSettlementTransfer } from '@/utils/settlement-transfer'; +import { + contractApprovalStatusOptions, + contractCategoryOptions, + contractStageOptions, + effectiveTypeOptions, + signTypeOptions, +} from '@/option/business/common'; + +const contractSelectionColumns = [ + { prop: 'contractNo', label: '合同编号', minWidth: 180 }, + { prop: 'contractName', label: '合同名称', minWidth: 220 }, + { prop: 'projectName', label: '所属项目', minWidth: 170 }, + { prop: 'organizationName', label: '所属组织', minWidth: 170 }, + { prop: 'contractCategory', label: '合同类别', minWidth: 130 }, + { prop: 'signType', label: '签约类型', minWidth: 120 }, + { prop: 'partyA', label: '甲方', minWidth: 170 }, + { prop: 'partyB', label: '乙方', minWidth: 170 }, + { prop: 'startDate', label: '开始日期', minWidth: 130 }, + { prop: 'endDate', label: '结束日期', minWidth: 130 }, + { prop: 'temporaryStartDate', label: '临时效力起', minWidth: 130 }, + { prop: 'temporaryEndDate', label: '临时效力止', minWidth: 130 }, +]; const ADJUST_BILLING_ELEMENTS = [ '按重量', @@ -777,15 +991,22 @@ export default { priceUnitRequest: null, changeRows: [], changePage: { current: 1, size: 10, total: 0 }, - changeDialog: { loading: false }, + latestChangeRows: [], + latestChangeLoading: false, + changeRecordsDialog: { visible: false, loading: false }, updateFeeDialog: { visible: false, submitting: false, row: null }, updateFeeForm: { contractId: '', billingPlanId: '' }, updateFeeRules: { contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }], }, contractOptions: [], - contractLoading: false, updateContractOptions: [], + contractCategoryOptions, + signTypeOptions, + effectiveTypeOptions, + contractStageOptions, + contractApprovalStatusOptions, + contractSelectionColumns, transportTypeOptions: [], billingPlanOptions: [], transferDialog: { visible: false, loading: false, submitting: false }, @@ -796,6 +1017,25 @@ export default { transferPage: { current: 1, size: 10, total: 0 }, generateDialog: { visible: false, loading: false }, generateQuery: {}, + generateContractDialog: { + visible: false, + loading: false, + expanded: false, + rows: [], + current: null, + query: { + contractNo: '', + contractName: '', + projectName: '', + organizationName: '', + contractCategory: '', + signType: '', + effectiveType: '', + contractStage: '', + approvalStatus: '', + }, + page: { current: 1, size: 10, total: 0 }, + }, generateRows: [], generateSelection: [], generatePage: { current: 1, size: 10, total: 0 }, @@ -805,6 +1045,30 @@ export default { }; }, computed: { + generateContractName() { + const contract = this.contractOptions.find( + item => String(item.id) === String(this.generateQuery.contractId) + ); + return contract?.contractName || ''; + }, + generateContractCategory() { + return this.settlementType === 'payable' ? '承运商合同' : '客户合同'; + }, + contractCategoryName(value) { + return ( + this.contractCategoryOptions.find(item => String(item.value) === String(value))?.label || + this.formatCell(value) + ); + }, + signTypeName(value) { + return ( + this.signTypeOptions.find(item => String(item.value) === String(value))?.label || + this.formatCell(value) + ); + }, + displayValue(value) { + return this.formatCell(value); + }, settlementTypeLabel() { if (this.settlementType === 'receivable') return '应收'; if (this.settlementType === 'payable') return '应付'; @@ -830,14 +1094,31 @@ export default { column => this.settlementType !== 'receivable' || column.prop !== 'preSettlementNo' ); const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText'); - const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({ - label: name, - prop: `tableFeeItem${index}`, - feeItemName: name, - dynamic: true, - minWidth: 130, - align: 'right', - })); + const dynamicColumns = this.isPayable + ? [ + { + label: '运输费', + prop: 'tableFreightAmount', + feeSummaryType: 'freight', + minWidth: 130, + align: 'right', + }, + { + label: '其他费用', + prop: 'tableOtherFeeAmount', + feeSummaryType: 'other', + minWidth: 130, + align: 'right', + }, + ] + : this.tableFeeItemNames.map((name, index) => ({ + label: name, + prop: `tableFeeItem${index}`, + feeItemName: name, + dynamic: true, + minWidth: 130, + align: 'right', + })); if (totalIndex < 0) return [...columns, ...dynamicColumns]; columns.splice(totalIndex, 0, ...dynamicColumns); return columns; @@ -846,12 +1127,14 @@ export default { return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns]; }, adjustFeeColumns() { - return [ - ...feeDetailBaseColumns, - ...this.adjustDynamicFeeColumns, - ...feeDetailTailColumns, - { label: '变更原因', prop: 'changeReason', minWidth: 220 }, - ]; + const tailColumns = feeDetailTailColumns.filter(column => column.prop !== 'remark'); + const afterAmountIndex = tailColumns.findIndex(column => column.prop === 'afterAmountText'); + tailColumns.splice(afterAmountIndex + 1, 0, { + label: '调整原因', + prop: 'changeReason', + minWidth: 220, + }); + return [...feeDetailBaseColumns, ...this.adjustDynamicFeeColumns, ...tailColumns]; }, transportCargoTypeOptions() { return this.cargoTypeOptions.filter(item => item.children?.length); @@ -1114,6 +1397,12 @@ export default { formatColumnValue(row, column) { if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]); if (column.prop === 'transportQuantity') return this.fixedTwoDecimals(row[column.prop]); + if (column.feeSummaryType) { + return this.money( + this.summarizeTableFee(row, column.feeSummaryType), + row.currency || 'RMB' + ); + } if (column.dynamic) { return this.money( this.normalizeFeeItems(row.feeItems)[column.feeItemName], @@ -1137,25 +1426,80 @@ export default { const res = await api.getList(this.page.current, this.page.size, params); const data = this.unwrapPage(res); this.rows = (data.records || []).map(this.decorateRow); - this.tableFeeItemNames = this.collectFeeItemNames(this.rows); + this.tableFeeItemNames = this.isPayable ? [] : this.collectFeeItemNames(this.rows); this.page.total = data.total || 0; } finally { this.loading = false; } }, - async loadContracts() { - const contractCategory = this.settlementType === 'payable' ? '承运商合同' : '客户合同'; - this.contractLoading = true; + openGenerateContractDialog() { + this.generateContractDialog.visible = true; + this.generateContractDialog.expanded = false; + this.generateContractDialog.current = null; + this.generateContractDialog.page.current = 1; + this.loadGenerateContracts(); + }, + async loadGenerateContracts() { + this.generateContractDialog.loading = true; try { - const res = await getContractList(1, 999, { - contractCategory, - }); + const res = await getContractList( + this.generateContractDialog.page.current, + this.generateContractDialog.page.size, + { + ...this.generateContractDialog.query, + contractCategory: this.generateContractCategory, + approvalStatuses: 'approved,change_approved', + } + ); const data = this.unwrapPage(res); - this.contractOptions = data.records || []; + this.generateContractDialog.rows = data.records || []; + this.generateContractDialog.page.total = Number(data.total || 0); } finally { - this.contractLoading = false; + this.generateContractDialog.loading = false; } }, + searchGenerateContracts() { + this.generateContractDialog.page.current = 1; + this.loadGenerateContracts(); + }, + resetGenerateContractQuery() { + this.generateContractDialog.query = { + contractNo: '', + contractName: '', + projectName: '', + organizationName: '', + contractCategory: '', + signType: '', + effectiveType: '', + contractStage: '', + approvalStatus: '', + }; + this.searchGenerateContracts(); + }, + handleGenerateContractSizeChange() { + this.generateContractDialog.page.current = 1; + this.loadGenerateContracts(); + }, + selectGenerateContract(row) { + if (!row) { + this.$message.warning('请选择合同'); + return; + } + const contract = { + ...row, + deptId: row.deptId || row.organizationId, + deptName: row.deptName || row.organizationName, + partyA: row.partyA || row.payerName, + partyB: row.partyB || row.payeeName, + }; + const index = this.contractOptions.findIndex(item => String(item.id) === String(contract.id)); + if (index >= 0) this.contractOptions.splice(index, 1, contract); + else this.contractOptions.push(contract); + this.generateQuery.contractId = contract.id; + this.handleGenerateContractChange(contract.id); + this.generateContractDialog.visible = false; + this.generateContractDialog.current = null; + }, handleSearch() { this.page.current = 1; this.loadTable(); @@ -1177,6 +1521,7 @@ export default { closeDetailPanel() { this.detailDialog.visible = false; this.detailDialog.row = null; + this.changeRecordsDialog.visible = false; }, closeAdjustPanel() { this.clearAdjustCalculations(); @@ -1525,11 +1870,31 @@ export default { } }, handleDetailTabChange(name) { - if (name === 'change') this.loadChangeRecords(); + if (name === 'change') this.loadLatestChangeRecord(); + }, + async loadLatestChangeRecord() { + if (!this.detailDialog.row) return; + this.latestChangeLoading = true; + this.latestChangeRows = []; + try { + const res = await api.getChangeRecords(1, 1, { + detailId: this.detailDialog.row.id, + }); + const data = this.unwrapPage(res); + this.latestChangeRows = (data.records || []).slice(0, 1); + } finally { + this.latestChangeLoading = false; + } + }, + openChangeRecordsDialog() { + if (!this.detailDialog.row) return; + this.changeRecordsDialog.visible = true; + this.changePage.current = 1; + this.loadChangeRecords(); }, async loadChangeRecords() { if (!this.detailDialog.row) return; - this.changeDialog.loading = true; + this.changeRecordsDialog.loading = true; try { const res = await api.getChangeRecords(this.changePage.current, this.changePage.size, { detailId: this.detailDialog.row.id, @@ -1538,7 +1903,7 @@ export default { this.changeRows = data.records || []; this.changePage.total = data.total || 0; } finally { - this.changeDialog.loading = false; + this.changeRecordsDialog.loading = false; } }, handleChangeSizeChange() { @@ -1792,11 +2157,26 @@ export default { this.generateDialog.visible = true; this.generateQuery = {}; this.contractOptions = []; + this.generateContractDialog.query = { + contractNo: '', + contractName: '', + projectName: '', + organizationName: '', + contractCategory: '', + signType: '', + effectiveType: '', + contractStage: '', + approvalStatus: '', + }; + this.generateContractDialog.visible = false; + this.generateContractDialog.expanded = false; + this.generateContractDialog.rows = []; + this.generateContractDialog.current = null; + this.generateContractDialog.page = { current: 1, size: 10, total: 0 }; this.billingPlanOptions = []; this.generateRows = []; this.generateSelection = []; this.generatePage = { current: 1, size: 10, total: 0 }; - this.loadContracts(); }, handleGenerateContractChange(contractId) { const options = this.syncBillingPlanOptions(contractId); @@ -1911,6 +2291,7 @@ export default { row.vehicleNumber || sourceWaybill?.vehicleNo || '', + transportQuantityText: row.transportQuantityText ?? row.transportQuantity ?? '', ...dynamic, }; }); @@ -2032,6 +2413,16 @@ export default { return {}; } }, + summarizeTableFee(row, summaryType) { + const feeItemEntries = Object.entries(this.normalizeFeeItems(row.feeItems)); + const matchedEntries = feeItemEntries.filter(([name]) => + summaryType === 'freight' ? this.isFreightFeeItem(name) : !this.isFreightFeeItem(name) + ); + if (matchedEntries.length) { + return matchedEntries.reduce((total, [, amount]) => total + Number(amount || 0), 0); + } + return summaryType === 'freight' ? row.freightAmount : row.otherFeeAmount; + }, money(value, currency) { if (value === null || value === undefined || value === '') return '-'; return `${Number(value).toFixed(2)} ${currency}`; @@ -2160,6 +2551,12 @@ export default { padding: 16px 0; } +.settlement-detail-page__empty { + padding: 24px 0; + color: #909399; + text-align: center; +} + .settlement-detail-page__dialog-form { padding: 16px 20px; background: #fff; @@ -2212,6 +2609,39 @@ export default { width: 220px; } +.settlement-detail-page__contract-filter { + display: grid !important; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px 24px; + margin-bottom: 16px; + + :deep(.el-form-item) { + display: flex; + min-width: 0; + margin-bottom: 0; + } + + :deep(.el-form-item__content) { + flex: 1; + min-width: 0; + } + + :deep(.el-input), + :deep(.el-select) { + width: 100%; + } +} + +.settlement-detail-page__contract-filter-actions { + grid-column: 1 / -1; + width: 100%; + justify-content: flex-end; + + :deep(.el-form-item__content) { + justify-content: flex-end; + } +} + .settlement-detail-page__transfer-form { margin-bottom: 16px; padding: 12px 12px 4px;