This commit is contained in:
2026-08-31 10:38:21 +08:00
parent a10eea247f
commit a75ac1cfa4
13 changed files with 864 additions and 161 deletions
@@ -190,7 +190,7 @@
<div
class="settlement-detail-page__detail-panel-body settlement-detail-page__adjust-panel-body"
>
<div v-if="isReceivable" class="settlement-detail-page__adjust-toolbar">
<div v-if="isPayable" class="settlement-detail-page__adjust-toolbar">
<el-button type="primary" :disabled="adjustDialog.loading" @click="addAdjustFee">
新增费用
</el-button>
@@ -209,18 +209,26 @@
show-overflow-tooltip
>
<template #default="{ row }">
<span
v-if="row.manualFee && ['billingFactor', 'billingType'].includes(column.prop)"
>
-
</span>
<el-input
v-else-if="column.prop === 'cargoName' && row.manualFee"
<el-select
v-if="column.prop === 'cargoName' && row.manualFee"
v-model="row.cargoName"
class="settlement-detail-page__adjust-control"
:loading="commonCargoLoading"
clearable
maxlength="100"
placeholder="请输入"
/>
filterable
allow-create
default-first-option
placeholder="请选择或输入"
@visible-change="visible => visible && loadCommonCargoOptions()"
@change="value => handleAdjustCargoNameChange(row, value)"
>
<el-option
v-for="item in manualCargoNameOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span v-else-if="column.prop === 'cargoName'">
{{ formatDetailCell(row, column.prop) }}
</span>
@@ -241,6 +249,56 @@
<span v-else-if="column.prop === 'cargoType'">
{{ formatDetailCell(row, column.prop) }}
</span>
<el-select
v-else-if="column.prop === 'specification' && row.manualFee"
v-model="row.specification"
class="settlement-detail-page__adjust-control"
:loading="commonCargoLoading"
clearable
filterable
allow-create
default-first-option
placeholder="请选择或输入"
@visible-change="visible => visible && loadCommonCargoOptions()"
@change="value => handleAdjustSpecificationChange(row, value)"
>
<el-option
v-for="item in manualSpecificationOptions(row)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-else-if="column.prop === 'billingFactor' && row.manualFee"
v-model="row.billingFactor"
class="settlement-detail-page__adjust-control"
clearable
placeholder="请选择"
@change="handleAdjustBillingFactorChange(row)"
>
<el-option
v-for="item in adjustBillingElements"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select
v-else-if="column.prop === 'billingType' && row.manualFee"
v-model="row.billingType"
class="settlement-detail-page__adjust-control"
:disabled="!row.billingFactor"
clearable
placeholder="请选择"
>
<el-option
v-for="item in adjustBillingTypes(row)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-select
v-else-if="column.prop === 'priceUnit'"
v-model="row.priceUnit"
@@ -327,7 +385,12 @@
</div>
<footer class="settlement-detail-page__detail-panel-footer">
<el-button @click="closeAdjustPanel">取消</el-button>
<el-button type="primary" :loading="adjustDialog.submitting" @click="saveAdjustFee">
<el-button
type="primary"
:loading="adjustDialog.submitting"
:disabled="hasAdjustCalculationPending"
@click="saveAdjustFee"
>
保存
</el-button>
</footer>
@@ -392,7 +455,6 @@
v-model="transferForm.settlementBillType"
@change="handleTransferTypeChange"
>
<el-radio label="pre">预结算单</el-radio>
<el-radio label="formal">正式结算单</el-radio>
</el-radio-group>
</el-form-item>
@@ -621,6 +683,7 @@ import {
} from '@/option/settlement/receivable-payable-detail';
import * as api from '@/api/settlement/receivable-payable-detail';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { getContractOptions as getSettlementContractOptions } from '@/api/settlement/preSettlement';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
@@ -628,6 +691,26 @@ import { getDictionary } from '@/api/system/dictbiz';
import { downloadXls } from '@/utils/util';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
const ADJUST_BILLING_ELEMENTS = [
'按重量',
'按体积',
'按车辆',
'按里程',
'按吨·公里',
'固定金额(整单一口价)',
'按数量',
];
const ADJUST_BILLING_TYPE_MAP = {
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按车辆: ['固定单价'],
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'固定金额(整单一口价)': ['固定一口价'],
按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
};
export default {
props: {
settlementType: {
@@ -667,6 +750,10 @@ export default {
adjustRows: [],
adjustDynamicFeeColumns: [],
adjustTextProps: ['specification', 'model', 'billingFactor', 'billingType'],
adjustBillingElements: ADJUST_BILLING_ELEMENTS,
commonCargoOptions: [],
commonCargoLoading: false,
commonCargoRequest: null,
cargoTypeOptions: [],
cargoTypeFlatOptions: [],
cargoTypeLoading: false,
@@ -718,6 +805,12 @@ export default {
isReceivable() {
return this.settlementType === 'receivable';
},
isPayable() {
return this.settlementType === 'payable';
},
hasAdjustCalculationPending() {
return this.adjustRows.some(row => row.calculating);
},
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
@@ -725,7 +818,9 @@ export default {
return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
},
displayTableColumns() {
const columns = [...this.tableColumns];
const columns = this.tableColumns.filter(
column => this.settlementType !== 'receivable' || column.prop !== 'preSettlementNo'
);
const totalIndex = columns.findIndex(column => column.prop === 'totalAmountText');
const dynamicColumns = this.tableFeeItemNames.map((name, index) => ({
label: name,
@@ -760,6 +855,9 @@ export default {
this.loadPriceUnitOptions();
this.loadTable();
},
beforeUnmount() {
this.clearAdjustCalculations();
},
methods: {
async loadTransportTypeOptions() {
const res = await getDictionary({ code: 'transport_type' });
@@ -806,6 +904,82 @@ export default {
});
return this.priceUnitRequest;
},
loadCommonCargoOptions() {
if (this.commonCargoOptions.length) return Promise.resolve(this.commonCargoOptions);
if (this.commonCargoRequest) return this.commonCargoRequest;
this.commonCargoLoading = true;
this.commonCargoRequest = getCommonCargoList(1, 9999)
.then(res => {
this.commonCargoOptions = this.extractRecords(res);
return this.commonCargoOptions;
})
.finally(() => {
this.commonCargoLoading = false;
this.commonCargoRequest = null;
});
return this.commonCargoRequest;
},
commonCargoMatches(row) {
const cargoType = String(row.cargoType || '').trim();
if (!cargoType) return this.commonCargoOptions;
const matched = this.commonCargoOptions.filter(item =>
[item.secondCargoTypeName, item.firstCargoTypeName, item.cargoType].some(
value => String(value || '').trim() === cargoType
)
);
return matched.length ? matched : this.commonCargoOptions;
},
manualCargoNameOptions(row) {
const options = new Map();
this.commonCargoMatches(row).forEach(item => {
const value = String(item.cargoName || '').trim();
if (value && !options.has(value)) options.set(value, { label: value, value });
});
return Array.from(options.values());
},
manualSpecificationOptions(row) {
const cargoName = String(row.cargoName || '').trim();
const records = cargoName
? this.commonCargoMatches(row).filter(
item => String(item.cargoName || '').trim() === cargoName
)
: this.commonCargoMatches(row);
const options = new Map();
records.forEach(item => {
const value = String(item.specification || item.spec || '').trim();
if (value && !options.has(value)) options.set(value, { label: value, value });
});
return Array.from(options.values());
},
handleAdjustCargoNameChange(row, value) {
row.cargoName = String(value || '').trim();
const cargo = this.commonCargoMatches(row).find(
item => String(item.cargoName || '').trim() === row.cargoName
);
if (cargo) this.applyCommonCargoToAdjustRow(row, cargo);
},
handleAdjustSpecificationChange(row, value) {
row.specification = String(value || '').trim();
const cargo = this.commonCargoMatches(row).find(
item =>
String(item.cargoName || '').trim() === String(row.cargoName || '').trim() &&
String(item.specification || item.spec || '').trim() === row.specification
);
if (cargo) this.applyCommonCargoToAdjustRow(row, cargo);
},
applyCommonCargoToAdjustRow(row, cargo) {
row.cargoName = cargo.cargoName || row.cargoName || '';
row.specification = cargo.specification || cargo.spec || row.specification || '';
row.model = cargo.model || row.model || '';
row.priceUnit = cargo.priceUnit || row.priceUnit || '';
row.cargoType =
cargo.secondCargoTypeName ||
cargo.cargoType ||
cargo.firstCargoTypeName ||
row.cargoType ||
'';
row.cargoTypePath = this.resolveCargoTypePath(row.cargoType);
},
buildCargoTypeTree(cargoTypes = []) {
const flatCargoTypes = [];
const collectCargoTypes = list => {
@@ -920,6 +1094,8 @@ export default {
row.cargoTypePath = path;
row.cargoType = labels.length ? labels[labels.length - 1] : '';
row.cargoName = '';
row.specification = '';
row.model = '';
},
formatColumnValue(row, column) {
if (column.prop === 'transportType') return this.transportTypeLabel(row[column.prop]);
@@ -989,6 +1165,7 @@ export default {
this.detailDialog.row = null;
},
closeAdjustPanel() {
this.clearAdjustCalculations();
this.adjustDialog.visible = false;
this.adjustDialog.row = null;
},
@@ -1041,6 +1218,7 @@ export default {
try {
const [res] = await Promise.all([
api.getFeeDetail(row.id),
this.loadCommonCargoOptions(),
this.loadCargoTypeOptions(),
this.loadPriceUnitOptions(),
]);
@@ -1068,9 +1246,14 @@ export default {
originalAmount: Number(item.originalAmount || 0),
feeItems,
dataSource:
item.dataSource || (item.billingFactor === '手工调整' ? '手录入' : '自动生成'),
manualFee: item.dataSource === '手工录入' || item.billingFactor === '手工调整',
item.dataSource || (item.billingFactor === '手工调整' ? '手录入' : '自动生成'),
manualFee:
['手动录入', '手动添加', '手工录入'].includes(item.dataSource) ||
item.billingFactor === '手工调整',
cargoTypePath: this.resolveCargoTypePath(item.cargoType),
calculating: false,
adjustCalculateTimer: null,
adjustCalculateVersion: 0,
};
this.recalculateAdjustRow(adjusted);
return adjusted;
@@ -1080,20 +1263,20 @@ export default {
}
},
addAdjustFee() {
if (!this.isReceivable) return;
if (!this.isPayable) return;
const feeItems = this.adjustDynamicFeeColumns.reduce((items, column) => {
items[column.feeItemName] = 0;
return items;
}, {});
this.adjustRows.push({
dataSource: '手录入',
dataSource: '手录入',
cargoName: '',
cargoType: '',
cargoTypePath: [],
specification: '',
model: '',
billingFactor: '-',
billingType: '-',
billingFactor: '',
billingType: '',
transportQuantity: '',
priceUnit: '',
unitPrice: '',
@@ -1107,10 +1290,19 @@ export default {
manualFee: true,
remark: '',
changeReason: '',
calculating: false,
adjustCalculateTimer: null,
adjustCalculateVersion: 0,
});
},
feeSourceLabel(value) {
return value === '手工录入' ? '手录入' : '自动生成';
return ['手动录入', '手动添加', '手工录入'].includes(value) ? '手录入' : '自动生成';
},
adjustBillingTypes(row) {
return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || [];
},
handleAdjustBillingFactorChange(row) {
if (!this.adjustBillingTypes(row).includes(row.billingType)) row.billingType = '';
},
adjustTextMaxlength(prop) {
return ['billingFactor', 'billingType'].includes(prop) ? 100 : 255;
@@ -1131,6 +1323,10 @@ export default {
},
handleAdjustDecimalInput(row, prop, value, changedField) {
row[prop] = this.normalizeAdjustDecimal(value);
if (!row.manualFee && row.id && ['transportQuantity', 'mileage'].includes(prop)) {
this.scheduleAdjustFeeCalculation(row);
return;
}
this.recalculateAdjustRow(row, changedField);
},
handleAdjustFeeItemInput(row, feeItemName, value) {
@@ -1155,6 +1351,50 @@ export default {
);
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
},
scheduleAdjustFeeCalculation(row) {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
row.adjustCalculateVersion += 1;
row.calculating = true;
const version = row.adjustCalculateVersion;
row.adjustCalculateTimer = setTimeout(() => {
row.adjustCalculateTimer = null;
this.calculateAdjustRow(row, version);
}, 300);
},
async calculateAdjustRow(row, version) {
try {
const res = await api.calculateAdjustedFee({
detailId: this.adjustDialog.row?.id,
feeId: row.id,
transportQuantity: Number(row.transportQuantity || 0),
mileage: Number(row.mileage || 0),
freightAmount: Number(row.freightAmount || 0),
feeItems: Object.fromEntries(
Object.entries(row.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
),
});
if (version !== row.adjustCalculateVersion || !this.adjustDialog.visible) return;
const data = res.data?.data || res.data || res || {};
row.freightAmount = Number(data.freightAmount || 0);
row.feeItems = Object.fromEntries(
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
);
row.adjustAmount = Number(data.adjustAmount || 0);
row.afterAmount = Number(data.afterAmount || 0);
} catch (error) {
row.calculateError = error.message;
} finally {
if (version === row.adjustCalculateVersion) row.calculating = false;
}
},
clearAdjustCalculations() {
this.adjustRows.forEach(row => {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
row.adjustCalculateTimer = null;
row.adjustCalculateVersion += 1;
row.calculating = false;
});
},
isFreightFeeItem(name) {
return String(name || '').includes('运费') || String(name || '').includes('运输费');
},
@@ -1163,15 +1403,20 @@ export default {
this.$message.warning('没有可调整的费用明细');
return;
}
if (this.hasAdjustCalculationPending) {
this.$message.warning('费用正在重新计算,请稍候');
return;
}
if (!this.validateManualAdjustRows()) return;
this.adjustDialog.submitting = true;
try {
await api.adjustFee({
detailId: this.adjustDialog.row.id,
rows: this.adjustRows.map(row => ({
id: row.id,
cargoName: row.cargoName,
cargoName: String(row.cargoName || '').trim(),
cargoType: row.cargoType,
specification: row.specification,
specification: String(row.specification || '').trim(),
model: row.model,
billingFactor: row.billingFactor,
billingType: row.billingType,
@@ -1193,6 +1438,32 @@ export default {
this.adjustDialog.submitting = false;
}
},
validateManualAdjustRows() {
for (const [index, row] of this.adjustRows.entries()) {
if (!row.manualFee) continue;
if (!String(row.cargoName || '').trim()) {
this.$message.warning(`第${index + 1}行货物名称不能为空`);
return false;
}
if (String(row.cargoName).trim().length > 100) {
this.$message.warning(`第${index + 1}行货物名称不能超过100个字`);
return false;
}
if (String(row.specification || '').trim().length > 255) {
this.$message.warning(`第${index + 1}行规格不能超过255个字`);
return false;
}
if (!ADJUST_BILLING_ELEMENTS.includes(row.billingFactor)) {
this.$message.warning(`第${index + 1}行请选择计费要素`);
return false;
}
if (!this.adjustBillingTypes(row).includes(row.billingType)) {
this.$message.warning(`第${index + 1}行请选择计费要素对应的计费类型`);
return false;
}
}
return true;
},
async loadFeeDetail() {
if (!this.detailDialog.row) return;
this.detailDialog.loading = true;
@@ -1841,6 +2112,7 @@ export default {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.settlement-detail-page__pagination {