✨ 结算付款体验优化与合同/运单导入校验
付款申请汇票筛选与提示去重、结算菜单拆分与金蝶同步弹框、应收应付详情高度、合同计费与半月结调整,以及导入运单批量删除校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user