✨ 完善付款申请多结算行、结算导入编辑与计费规则展示

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 12:06:51 +08:00
parent 1ac741f785
commit aee661772b
8 changed files with 884 additions and 217 deletions
@@ -149,7 +149,7 @@
>
<template #default="{ row }">
<div
v-if="isFreightFeeColumn(column)"
v-if="isChargeFeeColumn(column)"
class="settlement-detail-page__billing-rule-cell"
>
<span>{{ formatFeeDetailCell(row, column.prop) }}</span>
@@ -157,7 +157,7 @@
v-if="hasBillingRule(row)"
class="settlement-detail-page__billing-rule-icon"
title="查看计费规则详情"
@click.stop="openBillingRuleInfo(row)"
@click.stop="openBillingRuleInfo(row, chargeFeeName(column))"
>
<InfoFilled />
</el-icon>
@@ -376,12 +376,14 @@
inputmode="decimal"
@input="value => handleAdjustDecimalInput(row, 'transportQuantity', value)"
/>
<span v-else-if="column.prop === 'unitPrice' && isTierUnitPrice(row)">-</span>
<el-input
v-else-if="column.prop === 'unitPrice'"
class="settlement-detail-page__adjust-input"
:model-value="row.unitPrice"
:disabled="!isUnitPriceAdjustable(row)"
inputmode="decimal"
@input="value => handleAdjustDecimalInput(row, 'unitPrice', value)"
@input="value => handleAdjustDecimalInput(row, 'unitPrice', value, 'unitPrice')"
/>
<el-input
v-else-if="column.prop === 'mileage'"
@@ -1034,6 +1036,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { exportBlob } from '@/api/common';
import { getDictionary } from '@/api/system/dictbiz';
import { formatContractOptionLabel } from '@/utils/contract';
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
import { downloadXls } from '@/utils/util';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
import {
@@ -1221,7 +1224,9 @@ export default {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
feeDetailColumns() {
const baseColumns = feeDetailBaseColumns.filter(column => column.prop !== 'freightAmount');
const baseColumns = feeDetailBaseColumns.filter(
column => !['freightAmount', 'billingFactor', 'billingType'].includes(column.prop)
);
const hasAdjustment = this.feeRows.some(row => Number(row.adjustAmount || 0) !== 0);
const tailColumns = feeDetailTailColumns.map(col =>
col.prop === 'originalAmountText' && hasAdjustment
@@ -1302,17 +1307,13 @@ export default {
this.openRouteDetail();
},
isBottomPanelOpen() {
this.$nextTick(() => this.updateListTableHeight());
this.updateListTableHeight();
},
searchVisible() {
if (this.isBottomPanelOpen) {
this.$nextTick(() => this.updateListTableHeight());
}
this.updateListTableHeight();
},
searchExpanded() {
if (this.isBottomPanelOpen) {
this.$nextTick(() => this.updateListTableHeight());
}
this.updateListTableHeight();
},
displayTableColumns: {
immediate: true,
@@ -1343,6 +1344,10 @@ export default {
this.loadPriceUnitOptions();
await this.loadTable();
await this.openRouteDetail();
this.updateListTableHeight();
},
activated() {
this.updateListTableHeight();
},
// 标签切走(keep-alive 缓存)或销毁时,收起 teleport/append-to-body 弹层,避免残留盖住其它页面
deactivated() {
@@ -1353,6 +1358,8 @@ export default {
this.closeInnerDialogs();
},
methods: {
isTierUnitPrice,
isUnitPriceAdjustable,
closeInnerDialogs() {
this.closeDetailPanel();
this.closeAdjustPanel();
@@ -1363,24 +1370,38 @@ export default {
this.previewDialog.visible = false;
this.generateContractDialog.visible = false;
},
/** 底部面板打开时按剩余视口压缩主表高度,关闭后恢复自适应 */
/** 表格和分页撑满剩余可视区域;单据详情展开时贴住详情面板,关闭后恢复 */
updateListTableHeight() {
if (!this.isBottomPanelOpen) {
this.listTableHeight = undefined;
this.$nextTick(() => this.$refs.mainTableRef?.doLayout?.());
return;
this.$nextTick(() => {
window.requestAnimationFrame(() => this.applyListTableHeight());
});
},
applyListTableHeight() {
const tableElement = this.$refs.mainTableRef?.$el;
if (!tableElement) return;
const pagination = this.$el?.querySelector?.('.settlement-detail-page__pagination');
const paginationHeight = pagination?.offsetHeight || 0;
const tableTop = tableElement.getBoundingClientRect().top;
const view = this.$el?.closest?.('#avue-view');
const viewRect = view?.getBoundingClientRect?.();
let bottomEdge = viewRect ? viewRect.bottom - 12 : window.innerHeight;
if (this.isBottomPanelOpen) {
const panelRect = document
.querySelector('.settlement-detail-page__detail-panel')
?.getBoundingClientRect?.();
if (panelRect && panelRect.height > 0) {
bottomEdge = panelRect.top;
this.listTableHeightRetry = 0;
} else if ((this.listTableHeightRetry || 0) < 5) {
this.listTableHeightRetry = (this.listTableHeightRetry || 0) + 1;
window.requestAnimationFrame(() => this.applyListTableHeight());
return;
}
} else {
this.listTableHeightRetry = 0;
}
const tablePanel = this.$el?.querySelector?.('.settlement-detail-page__table-panel');
const toolbar = tablePanel?.querySelector('.settlement-detail-page__toolbar');
const pagination = tablePanel?.querySelector('.settlement-detail-page__pagination');
const panelTop = tablePanel?.getBoundingClientRect?.()?.top ?? 200;
const toolbarHeight = toolbar?.offsetHeight ?? 56;
const paginationHeight = pagination?.offsetHeight ?? 64;
// 与 .settlement-detail-page__detail-panel 的 max-height: min(60vh, 560px) 对齐
const bottomPanelHeight = Math.min(window.innerHeight * 0.6, 560);
const available =
window.innerHeight - panelTop - toolbarHeight - paginationHeight - bottomPanelHeight - 8;
this.listTableHeight = Math.max(180, Math.floor(available));
const gap = this.isBottomPanelOpen ? 8 : 0;
this.listTableHeight = Math.max(180, Math.floor(bottomEdge - tableTop - paginationHeight - gap));
this.$nextTick(() => this.$refs.mainTableRef?.doLayout?.());
},
contractCategoryName(value) {
@@ -1982,8 +2003,9 @@ export default {
},
handleAdjustDecimalInput(row, prop, value, changedField) {
row[prop] = this.normalizeAdjustDecimal(value);
if (!row.manualFee && row.id && ['transportQuantity', 'mileage'].includes(prop)) {
this.scheduleAdjustFeeCalculation(row);
if (!row.manualFee && row.id && ['transportQuantity', 'mileage', 'unitPrice'].includes(prop)) {
if (prop === 'unitPrice' && !isUnitPriceAdjustable(row)) return;
this.scheduleAdjustFeeCalculation(row, prop);
return;
}
this.recalculateAdjustRow(row, changedField);
@@ -2010,23 +2032,25 @@ export default {
);
row.adjustAmount = Number((row.afterAmount - Number(row.originalAmount || 0)).toFixed(2));
},
scheduleAdjustFeeCalculation(row) {
scheduleAdjustFeeCalculation(row, changedField) {
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);
this.calculateAdjustRow(row, version, changedField);
}, 300);
},
async calculateAdjustRow(row, version) {
async calculateAdjustRow(row, version, changedField) {
try {
const res = await api.calculateAdjustedFee({
detailId: this.adjustDialog.row?.id,
feeId: row.id,
transportQuantity: Number(row.transportQuantity || 0),
mileage: Number(row.mileage || 0),
unitPrice: row.unitPrice === null || row.unitPrice === '' ? null : Number(row.unitPrice),
changedField,
freightAmount: Number(row.freightAmount || 0),
feeItems: Object.fromEntries(
Object.entries(row.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
@@ -2034,18 +2058,24 @@ export default {
});
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);
this.applyCalculatedAdjustRow(row, data);
} catch (error) {
row.calculateError = error.message;
} finally {
if (version === row.adjustCalculateVersion) row.calculating = false;
}
},
applyCalculatedAdjustRow(row, data) {
row.freightAmount = Number(data.freightAmount || 0);
row.feeItems = Object.fromEntries(
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
);
if (!isTierUnitPrice(row) && data.unitPrice !== undefined && data.unitPrice !== null) {
row.unitPrice = Number(data.unitPrice);
}
row.adjustAmount = Number(data.adjustAmount || 0);
row.afterAmount = Number(data.afterAmount || 0);
},
clearAdjustCalculations() {
this.adjustRows.forEach(row => {
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
@@ -2057,12 +2087,14 @@ export default {
isFreightFeeItem(name) {
return String(name || '').includes('运费') || String(name || '').includes('运输费');
},
isFreightFeeColumn(column = {}) {
return (
column.prop === 'freightAmount' ||
this.isFreightFeeItem(column.label) ||
this.isFreightFeeItem(column.feeItemName)
);
isChargeFeeColumn(column = {}) {
if (column.prop === 'priceUnit') return false;
if (column.prop === 'freightAmount') return true;
return String(column.prop || '').startsWith('feeItem');
},
chargeFeeName(column = {}) {
if (column.prop === 'freightAmount') return column.label || '运输费';
return column.label || '';
},
hasBillingRule(row) {
const rule = this.normalizeBillingRule(row);
@@ -2078,11 +2110,21 @@ export default {
rule.matchCondition?.cargoType
);
},
openBillingRuleInfo(row) {
this.billingRuleDialog.rule = this.normalizeBillingRule(row);
openBillingRuleInfo(row, feeName = '') {
this.billingRuleDialog.rule = this.normalizeBillingRule(row, feeName);
this.billingRuleDialog.visible = true;
},
normalizeBillingRule(row) {
ruleFeeName(rule = {}) {
return String(rule.feeItem || rule.feeItemName || rule.name || '').trim();
},
feeNamesMatch(left, right) {
const source = String(left || '').trim();
const target = String(right || '').trim();
if (!source || !target) return false;
if (source === target) return true;
return this.isFreightFeeItem(source) && this.isFreightFeeItem(target);
},
normalizeBillingRule(row, feeName = '') {
const source = row || {};
const nested = [
source.billingRule,
@@ -2095,28 +2137,48 @@ export default {
source.rule,
].find(value => value !== undefined && value !== null && value !== '');
const parsedNested = this.parseJsonValue(nested);
const matchedRules = Array.isArray(parsedNested)
const parsedRules = Array.isArray(parsedNested)
? parsedNested.filter(item => item && typeof item === 'object')
: [];
let rule = this.parseObject(matchedRules[0] || parsedNested || nested);
if (!Object.keys(rule).length && typeof nested === 'string') rule = { name: nested };
const requestedFeeName = String(feeName || '').trim();
const matchedRules = requestedFeeName
? parsedRules.filter(item => this.feeNamesMatch(this.ruleFeeName(item), requestedFeeName))
: parsedRules;
const sharedRule =
parsedRules.length === 1 && !this.ruleFeeName(parsedRules[0]) ? parsedRules[0] : null;
const ruleSource =
matchedRules[0] || sharedRule || (!requestedFeeName ? parsedNested || nested : null);
let rule = this.parseObject(ruleSource);
if (!Object.keys(rule).length && typeof nested === 'string' && !requestedFeeName) {
rule = { name: nested };
}
const matchCondition = this.parseObject(
source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition
requestedFeeName
? (rule.matchCondition ?? source.matchCondition ?? source.billingMatchCondition)
: (source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition)
);
return {
...rule,
feeItem: source.billingFeeItem || source.feeItem || rule.feeItem,
billingElement:
source.billingElement ||
source.billingFactor ||
rule.billingElement ||
rule.billingFactor,
billingType: source.billingType || rule.billingType,
billingUnit: source.billingUnit || source.priceUnit || rule.billingUnit || rule.priceUnit,
unitPrice: source.billingUnitPrice ?? source.unitPrice ?? rule.unitPrice,
minimumBillingWeight:
source.minimumBillingWeight ?? rule.minimumBillingWeight ?? rule.lowerLimit,
remark: source.billingRuleRemark || source.ruleRemark || rule.remark,
feeItem: requestedFeeName || source.billingFeeItem || source.feeItem || rule.feeItem,
billingElement: requestedFeeName
? rule.billingElement || rule.billingFactor || ''
: source.billingElement ||
source.billingFactor ||
rule.billingElement ||
rule.billingFactor,
billingType: requestedFeeName ? rule.billingType || '' : source.billingType || rule.billingType,
billingUnit: requestedFeeName
? rule.billingUnit || rule.priceUnit || ''
: source.billingUnit || source.priceUnit || rule.billingUnit || rule.priceUnit,
unitPrice: requestedFeeName
? (rule.unitPrice ?? '')
: (source.billingUnitPrice ?? source.unitPrice ?? rule.unitPrice),
minimumBillingWeight: requestedFeeName
? (rule.minimumBillingWeight ?? rule.lowerLimit ?? '')
: (source.minimumBillingWeight ?? rule.minimumBillingWeight ?? rule.lowerLimit),
remark: requestedFeeName
? rule.remark || ''
: source.billingRuleRemark || source.ruleRemark || rule.remark,
rules: matchedRules,
matchCondition: {
...matchCondition,