✨ 结算付款体验优化与合同/运单导入校验

付款申请汇票筛选与提示去重、结算菜单拆分与金蝶同步弹框、应收应付详情高度、合同计费与半月结调整,以及导入运单批量删除校验。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-18 05:12:26 +08:00
parent 8db52ef168
commit 6f2383a7e5
36 changed files with 1387 additions and 495 deletions
@@ -459,7 +459,7 @@
</section-card>
<section-card v-if="form.id && readonly" title="变更记录">
<el-table :data="adjustChangeRecords" border>
<el-table :data="changeRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column
v-for="column in changeTableColumns"
@@ -753,20 +753,6 @@
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" align="center" />
<el-table-column label="计费规则" min-width="190" align="center">
<template #default="{ row }">
<div class="pre-settlement-editor__billing-rule-cell">
<span>{{ displayValue(billingRuleName(row)) }}</span>
<el-icon
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click="openBillingRuleInfo(row)"
>
<InfoFilled />
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column label="运输总量" min-width="150" align="center">
<template #default="{ row }">
<span v-if="adjustDialog.readonly">{{ formatQuantity(row) }}</span>
@@ -809,11 +795,33 @@
v-for="feeItem in adjustFeeItemNames"
:key="feeItem"
:label="feeItem"
min-width="140"
min-width="160"
align="center"
>
<template #default="{ row }">
<div
v-if="isFreightFeeItem(feeItem)"
class="pre-settlement-editor__billing-rule-cell"
>
<el-input-number
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
:controls="false"
:disabled="adjustDialog.readonly"
@change="recalculateAdjustRow(row, feeItem)"
/>
<el-icon
v-if="hasBillingRule(row)"
class="pre-settlement-editor__billing-rule-icon"
title="查看计费规则详情"
@click="openBillingRuleInfo(row)"
>
<InfoFilled />
</el-icon>
</div>
<el-input-number
v-else
v-model="row.feeItems[feeItem]"
:min="0"
:precision="2"
@@ -833,11 +841,6 @@
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="200" align="center">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="200" :disabled="adjustDialog.readonly" />
</template>
</el-table-column>
</el-table>
<el-form
v-if="!adjustDialog.readonly"
@@ -846,7 +849,7 @@
label-width="auto"
class="pre-settlement-editor__adjust-reason"
>
<el-form-item label="调整原因">
<el-form-item label="备注">
<el-input
v-model="adjustDialog.reason"
type="textarea"
@@ -885,12 +888,6 @@
<template v-if="billingRuleDialog.rule">
<div class="dialog-section-title">计费规则</div>
<el-descriptions :column="2" border class="pre-settlement-editor__billing-rule-detail">
<el-descriptions-item label="规则名称">
{{ displayValue(billingRuleDialog.rule.name) }}
</el-descriptions-item>
<el-descriptions-item label="费用类型">
{{ displayValue(billingRuleDialog.rule.feeType) }}
</el-descriptions-item>
<el-descriptions-item label="费用项">
{{ displayValue(billingRuleDialog.rule.feeItem) }}
</el-descriptions-item>
@@ -926,8 +923,8 @@
<el-descriptions-item label="目的地">
{{ displayValue(billingRuleDialog.rule.matchCondition.destination) }}
</el-descriptions-item>
<el-descriptions-item label="运输方式">
{{ displayValue(billingRuleDialog.rule.matchCondition.transportMode) }}
<el-descriptions-item label="货物名称">
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoName) }}
</el-descriptions-item>
<el-descriptions-item label="货物类型">
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoType) }}
@@ -1202,8 +1199,8 @@ export default {
return this.editable && this.hasPermission('pre_settlement_adjust');
},
dialogTitle() {
if (this.readonly) return '查看预结算单';
return this.form.id ? '编辑预结算单' : '新增预结算单';
if (this.readonly) return '查看预付结算单';
return this.form.id ? '编辑预付结算单' : '新增预付结算单';
},
settlementTypeName() {
return this.form.settlementType === 'receivable' ? '应收' : '应付';
@@ -1285,13 +1282,6 @@ export default {
});
return Array.from(names);
},
adjustChangeRecords() {
return this.changeRecords.filter(
row =>
row.changeType === '结算明细项' &&
String(row.lineNo ?? '') === String(this.adjustDialog.detailLineNo ?? '')
);
},
},
watch: {
modelValue: {
@@ -1865,7 +1855,25 @@ export default {
});
const manualFees = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
this.summaryFees = [...summaryMap.values(), ...manualFees];
const nextFeeMap = new Map();
summaryMap.forEach((row, key) => nextFeeMap.set(`0\u0000${key}`, row));
manualFees.forEach(row => {
const key = `1\u0000${row.feeType}\u0000${row.feeItem}`;
nextFeeMap.set(key, row);
});
// 按编辑前顺序回填,新增费用项追加到末尾,避免手工费用被统一挤到最后。
const orderedFees = [];
const previousKeys = this.summaryFees.map(
row => `${Number(row.manualFlag) === 1 ? 1 : 0}\u0000${row.feeType}\u0000${row.feeItem}`
);
previousKeys.forEach(key => {
const row = nextFeeMap.get(key);
if (!row) return;
orderedFees.push(row);
nextFeeMap.delete(key);
});
nextFeeMap.forEach(row => orderedFees.push(row));
this.summaryFees = orderedFees;
this.recalculateLocalAmount();
},
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
@@ -2053,7 +2061,7 @@ export default {
}
},
async removeDetailRow(row) {
await this.$confirm('确认将该明细踢出预结算单?', '提示', { type: 'warning' });
await this.$confirm('确认将该明细踢出预付结算单?', '提示', { type: 'warning' });
if (!this.form.id || !row.id || row.id === row.sourceDetailId) {
this.details = this.details.filter(item => item !== row);
this.buildSummaryFeesFromDetails();
@@ -2228,7 +2236,11 @@ export default {
: '';
const sourceDetailId = String(this.adjustDialog.sourceDetailId || '');
const pending = sourceDetailId ? this.pendingAdjustments[sourceDetailId] : null;
this.adjustDialog.reason = pending?.reason || '';
// 底部备注回显结算明细备注;暂存过的调整优先取暂存备注
this.adjustDialog.reason =
pending?.reason !== undefined && pending?.reason !== null
? pending.reason
: detailRow.remark || '';
try {
// 已暂存的调整结果优先回显,避免重新打开时又回到调整前的数据。
if (pending?.rows?.length) {
@@ -2295,8 +2307,19 @@ export default {
this.adjustChangeRecordDetailRows = this.buildAdjustChangeRecordRows(row);
this.adjustChangeRecordVisible = true;
},
billingRuleName(row) {
return this.normalizeBillingRule(row).name;
hasBillingRule(row) {
const rule = this.normalizeBillingRule(row);
return Boolean(
rule.feeItem ||
rule.billingElement ||
rule.billingType ||
rule.unitPrice ||
rule.rules?.length ||
rule.matchCondition?.origin ||
rule.matchCondition?.destination ||
rule.matchCondition?.cargoName ||
rule.matchCondition?.cargoType
);
},
openBillingRuleInfo(row) {
this.billingRuleDialog.rule = this.normalizeBillingRule(row);
@@ -2323,27 +2346,8 @@ export default {
const matchCondition = this.parseObject(
source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition
);
const ruleNames = matchedRules
.map(item => item.ruleName || item.name || item.billingRuleName || item.feeItem)
.filter(Boolean);
const name =
source.billingRuleName ||
source.feeRuleName ||
source.ruleName ||
source.billingPlanRuleName ||
source.billingPlanName ||
rule.name ||
rule.ruleName ||
rule.billingRuleName ||
rule.planName ||
rule.billingPlanName ||
ruleNames.join('、') ||
rule.feeItem ||
'';
return {
...rule,
name,
feeType: source.billingFeeType || source.feeType || rule.feeType,
feeItem: source.billingFeeItem || source.feeItem || rule.feeItem,
billingElement:
source.billingElement ||
@@ -2361,7 +2365,11 @@ export default {
...matchCondition,
origin: matchCondition.origin || matchCondition.originName,
destination: matchCondition.destination || matchCondition.destinationName,
transportMode: matchCondition.transportMode || matchCondition.transportType,
cargoName:
matchCondition.cargoName ||
matchCondition.cargoNameLabel ||
source.cargoName ||
rule.cargoName,
cargoType: matchCondition.cargoType || matchCondition.cargoTypeName,
},
};
@@ -2451,9 +2459,13 @@ export default {
reason: this.adjustDialog.reason,
rows: this.adjustRows.map(row => ({ ...row, feeItems: { ...row.feeItems } })),
};
this.applyLocalAdjustment(this.adjustDialog.sourceDetailId, this.adjustRows);
this.applyLocalAdjustment(
this.adjustDialog.sourceDetailId,
this.adjustRows,
this.adjustDialog.reason
);
this.adjustDialog.visible = false;
this.$message.success('结算明细调整已暂存,保存预结算单后生效');
this.$message.success('结算明细调整已暂存,保存预付结算单后生效');
return;
}
await adjustDetail({
@@ -2468,7 +2480,6 @@ export default {
feeItems: row.feeItems,
settlementAmountTax: row.settlementAmountTax,
settlementAmountNoTax: row.settlementAmountNoTax,
remark: row.remark,
})),
});
this.adjustDialog.visible = false;
@@ -2479,12 +2490,12 @@ export default {
this.adjustDialog.saving = false;
}
},
applyLocalAdjustment(sourceDetailId, rows) {
applyLocalAdjustment(sourceDetailId, rows, detailRemark) {
const detail = this.details.find(
item => String(item.sourceDetailId || item.id) === String(sourceDetailId)
);
if (!detail) return;
// 原金额以进入预结算单时的费用快照为基准,调整金额 = 结算金额 - 原金额。
// 原金额以进入预付结算单时的费用快照为基准,调整金额 = 结算金额 - 原金额。
const baseRows = this.sourceFeeSnapshots[String(sourceDetailId)] || [];
const originalAmount = baseRows.length
? baseRows.reduce((sum, row) => sum + Number(row.settlementAmountTax || 0), 0)
@@ -2493,20 +2504,48 @@ export default {
(sum, row) => sum + Number(row.settlementAmountTax || 0),
0
);
const settlementAmountNoTax = rows.reduce(
(sum, row) => sum + Number(row.settlementAmountNoTax || 0),
0
);
const hasSettlementAmountNoTax = rows.some(
row => row.settlementAmountNoTax !== undefined && row.settlementAmountNoTax !== null
);
const freightAmount = rows.reduce((sum, row) => sum + Number(row.freightAmount || 0), 0);
const transportQuantity = rows.reduce(
(sum, row) => sum + Number(row.transportQuantity || 0),
0
);
const mileage = rows.reduce((sum, row) => sum + Number(row.mileage || 0), 0);
const feeItems = {};
rows.forEach(row =>
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
})
);
const firstRow = rows[0] || {};
Object.assign(detail, {
transportQuantity: Number(transportQuantity.toFixed(2)),
mileage: Number(mileage.toFixed(2)),
unitPrice: Number(firstRow.unitPrice || 0),
originalAmount: Number(originalAmount.toFixed(2)),
adjustAmount: Number((settlementAmountTax - originalAmount).toFixed(2)),
settlementAmountTax: Number(settlementAmountTax.toFixed(2)),
settlementAmountNoTax: hasSettlementAmountNoTax
? Number(settlementAmountNoTax.toFixed(2))
: detail.settlementAmountNoTax,
freightAmount: Number(freightAmount.toFixed(2)),
feeItemsJson: JSON.stringify(feeItems),
// 底部备注写入结算明细备注
remark: detailRemark ?? '',
});
// 暂存调整后同步费用缓存,保证货物名称/类型拼接与列表展示一致。
if (detail.id) {
this.detailFeeCache[detail.id] = rows.map(row => ({
...row,
feeItems: { ...row.feeItems },
}));
}
this.buildSummaryFeesFromDetails();
},
async persistPendingAdjustments() {
@@ -2534,11 +2573,14 @@ export default {
feeItems: pendingRow.feeItems,
settlementAmountTax: pendingRow.settlementAmountTax,
settlementAmountNoTax: pendingRow.settlementAmountNoTax,
remark: pendingRow.remark,
};
});
if (rows.length) {
await adjustDetail({ detailId: detail.id, changeReason: pending.reason, rows });
await adjustDetail({
detailId: detail.id,
changeReason: pending.reason,
rows,
});
}
}
this.pendingAdjustments = {};
@@ -2610,7 +2652,7 @@ export default {
XLSX.utils.book_append_sheet(workbook, sheet, '结算明细');
XLSX.writeFile(
workbook,
`${this.form.preSettlementNo || '预结算单'}结算明细${this.$dayjs().format(
`${this.form.preSettlementNo || '预付结算单'}结算明细${this.$dayjs().format(
'YYYY-MM-DD HH-mm-ss'
)}.xlsx`
);