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

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

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
@@ -54,9 +54,11 @@
</div>
<el-table
ref="mainTableRef"
v-loading="loading"
:data="rows"
:size="tableSize"
:height="listTableHeight"
border
class="settlement-detail-page__table"
@selection-change="selectionChange"
@@ -146,7 +148,21 @@
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatFeeDetailCell(row, column.prop) }}
<div
v-if="isFreightFeeColumn(column)"
class="settlement-detail-page__billing-rule-cell"
>
<span>{{ formatFeeDetailCell(row, column.prop) }}</span>
<el-icon
v-if="hasBillingRule(row)"
class="settlement-detail-page__billing-rule-icon"
title="查看计费规则详情"
@click.stop="openBillingRuleInfo(row)"
>
<InfoFilled />
</el-icon>
</div>
<span v-else>{{ formatFeeDetailCell(row, column.prop) }}</span>
</template>
</el-table-column>
</el-table>
@@ -934,11 +950,70 @@
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="billingRuleDialog.visible"
title="计费规则详情"
width="720px"
append-to-body
destroy-on-close
>
<template v-if="billingRuleDialog.rule">
<div class="dialog-section-title">计费规则</div>
<el-descriptions :column="2" border class="settlement-detail-page__billing-rule-detail">
<el-descriptions-item label="费用项">
{{ displayValue(billingRuleDialog.rule.feeItem) }}
</el-descriptions-item>
<el-descriptions-item label="计费要素">
{{ displayValue(billingRuleDialog.rule.billingElement) }}
</el-descriptions-item>
<el-descriptions-item label="计费类型">
{{ displayValue(billingRuleDialog.rule.billingType) }}
</el-descriptions-item>
<el-descriptions-item label="计费单位">
{{ displayValue(billingRuleDialog.rule.billingUnit) }}
</el-descriptions-item>
<el-descriptions-item label="单价">
{{ displayValue(billingRuleDialog.rule.unitPrice) }}
</el-descriptions-item>
<el-descriptions-item label="最低计费量">
{{ displayValue(billingRuleDialog.rule.minimumBillingWeight) }}
</el-descriptions-item>
<el-descriptions-item label="计费区间">
{{ billingRuleRanges(billingRuleDialog.rule) }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ displayValue(billingRuleDialog.rule.remark) }}
</el-descriptions-item>
</el-descriptions>
<div class="dialog-section-title settlement-detail-page__billing-rule-match-title">
匹配条件
</div>
<el-descriptions :column="2" border class="settlement-detail-page__billing-rule-detail">
<el-descriptions-item label="起运地">
{{ displayValue(billingRuleDialog.rule.matchCondition.origin) }}
</el-descriptions-item>
<el-descriptions-item label="目的地">
{{ displayValue(billingRuleDialog.rule.matchCondition.destination) }}
</el-descriptions-item>
<el-descriptions-item label="货物名称">
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoName) }}
</el-descriptions-item>
<el-descriptions-item label="货物类型">
{{ displayValue(billingRuleDialog.rule.matchCondition.cargoType) }}
</el-descriptions-item>
</el-descriptions>
</template>
<el-empty v-else description="暂无计费规则详情" />
<template #footer>
<el-button type="primary" @click="billingRuleDialog.visible = false">关闭</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue';
import { ArrowDown, ArrowUp, Close, InfoFilled } from '@element-plus/icons-vue';
import {
changeRecordColumns,
feeDetailBaseColumns,
@@ -990,7 +1065,7 @@ const ADJUST_BILLING_TYPE_MAP = {
};
export default {
components: { SettlementTableTools },
components: { SettlementTableTools, InfoFilled },
props: {
settlementType: {
type: String,
@@ -1002,6 +1077,7 @@ export default {
ArrowDown,
ArrowUp,
Close,
InfoFilled,
searchFields,
tableColumns,
tableFeeItemNames: [],
@@ -1018,6 +1094,8 @@ export default {
rows: [],
selection: [],
page: { current: 1, size: 10, total: 0 },
// 底部详情/调整面板打开时压缩主表高度,露出表格滚动条
listTableHeight: undefined,
detailDialog: {
visible: false,
title: '费用明细',
@@ -1027,6 +1105,10 @@ export default {
},
feeRows: [],
dynamicFeeColumns: [],
billingRuleDialog: {
visible: false,
rule: null,
},
adjustDialog: { visible: false, loading: false, submitting: false, row: null },
adjustRows: [],
adjustDynamicFeeColumns: [],
@@ -1210,11 +1292,28 @@ export default {
transportCargoTypeOptions() {
return this.cargoTypeOptions.filter(item => item.children?.length);
},
/** 底部单据详情或调整面板是否展开(与 teleport 面板显示条件一致) */
isBottomPanelOpen() {
return Boolean(this.detailDialog.visible || this.adjustDialog.visible);
},
},
watch: {
'$route.fullPath'() {
this.openRouteDetail();
},
isBottomPanelOpen() {
this.$nextTick(() => this.updateListTableHeight());
},
searchVisible() {
if (this.isBottomPanelOpen) {
this.$nextTick(() => this.updateListTableHeight());
}
},
searchExpanded() {
if (this.isBottomPanelOpen) {
this.$nextTick(() => this.updateListTableHeight());
}
},
displayTableColumns: {
immediate: true,
handler(columns) {
@@ -1238,6 +1337,7 @@ export default {
},
},
async mounted() {
window.addEventListener('resize', this.updateListTableHeight);
this.loadTransportTypeOptions();
this.loadCargoTypeOptions();
this.loadPriceUnitOptions();
@@ -1249,6 +1349,7 @@ export default {
this.closeInnerDialogs();
},
beforeUnmount() {
window.removeEventListener('resize', this.updateListTableHeight);
this.closeInnerDialogs();
},
methods: {
@@ -1262,6 +1363,26 @@ export default {
this.previewDialog.visible = false;
this.generateContractDialog.visible = false;
},
/** 底部面板打开时按剩余视口压缩主表高度,关闭后恢复自适应 */
updateListTableHeight() {
if (!this.isBottomPanelOpen) {
this.listTableHeight = undefined;
this.$nextTick(() => this.$refs.mainTableRef?.doLayout?.());
return;
}
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));
this.$nextTick(() => this.$refs.mainTableRef?.doLayout?.());
},
contractCategoryName(value) {
return (
this.contractCategoryOptions.find(item => String(item.value) === String(value))?.label ||
@@ -1690,6 +1811,8 @@ export default {
this.detailDialog.visible = false;
this.detailDialog.row = null;
this.changeRecordsDialog.visible = false;
this.billingRuleDialog.visible = false;
this.billingRuleDialog.rule = null;
},
closeAdjustPanel() {
this.clearAdjustCalculations();
@@ -1934,6 +2057,108 @@ 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)
);
},
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);
this.billingRuleDialog.visible = true;
},
normalizeBillingRule(row) {
const source = row || {};
const nested = [
source.billingRule,
source.billingRuleInfo,
source.billingRuleJson,
source.billingRulesJson,
source.billingRules,
source.billingPlanRule,
source.feeRule,
source.rule,
].find(value => value !== undefined && value !== null && value !== '');
const parsedNested = this.parseJsonValue(nested);
const matchedRules = 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 matchCondition = this.parseObject(
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,
rules: matchedRules,
matchCondition: {
...matchCondition,
origin: matchCondition.origin || matchCondition.originName,
destination: matchCondition.destination || matchCondition.destinationName,
cargoName:
matchCondition.cargoName ||
matchCondition.cargoNameLabel ||
source.cargoName ||
rule.cargoName,
cargoType: matchCondition.cargoType || matchCondition.cargoTypeName,
},
};
},
billingRuleRanges(rule) {
const ranges = Array.isArray(rule?.limitRanges) ? rule.limitRanges : [];
if (ranges.length) {
return ranges
.map(
item => `${item.lowerLimit ?? '-'}~${item.upperLimit ?? '-'}:${item.unitPrice ?? '-'}`
)
.join(';');
}
if (rule?.lowerLimit !== undefined || rule?.upperLimit !== undefined) {
return `${rule.lowerLimit ?? '-'}~${rule.upperLimit ?? '-'}`;
}
return '-';
},
parseJsonValue(value) {
if (value === undefined || value === null || value === '') return null;
if (typeof value === 'object') return value;
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch (error) {
return value;
}
},
parseObject(value) {
const parsed = this.parseJsonValue(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
},
async saveAdjustFee() {
if (!this.adjustRows.length || !this.adjustDialog.row) {
this.$message.warning('没有可调整的费用明细');
@@ -3011,6 +3236,28 @@ export default {
padding-top: 16px;
}
.settlement-detail-page__billing-rule-cell {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
max-width: 100%;
}
.settlement-detail-page__billing-rule-icon {
flex: 0 0 auto;
color: var(--el-color-primary);
cursor: pointer;
}
.settlement-detail-page__billing-rule-detail {
margin-top: 12px;
}
.settlement-detail-page__billing-rule-match-title {
margin-top: 20px;
}
@media screen and (max-width: 992px) {
.settlement-detail-page__detail-panel {
left: 0;