@@ -121,7 +121,9 @@
/ >
< / el-select >
< el-input-number
v-else-if = "editable && column.prop === 'adjustAmount'"
v-else-if ="
editable && column.prop === 'adjustAmount' && Number(row.manualFlag) === 1
"
v-model="row.adjustAmount"
:class="{
'pre-settlement-editor__negative-amount': Number(row.adjustAmount || 0) < 0,
@@ -671,6 +673,20 @@
<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>
@@ -705,18 +721,6 @@
/>
</template>
</el-table-column>
< el-table-column label = "运费" min -width = " 140 " align = "center" >
< template # default = "{ row }" >
< el-input-number
v-model = "row.freightAmount"
:min = "0"
:precision = "2"
:controls = "false"
:disabled = "adjustDialog.readonly"
@change ="recalculateAdjustRow(row, 'freight')"
/ >
< / template >
< / el-table-column >
<el-table-column
v-for="feeItem in adjustFeeItemNames"
:key="feeItem"
@@ -774,10 +778,76 @@
</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="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>
<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 pre-settlement-editor__billing-rule-match-title">
匹配条件
</div>
<el-descriptions :column="2" border class="pre-settlement-editor__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.transportMode) }}
</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>
</template>
<script>
import { mapGetters } from 'vuex';
import { InfoFilled } from '@element-plus/icons-vue';
import {
adjustDetail,
getCandidateDetails,
@@ -814,6 +884,7 @@ import * as XLSX from 'xlsx';
export default {
name: 'PreSettlementEditor',
components: { InfoFilled },
props: {
modelValue: {
type: Boolean,
@@ -835,6 +906,10 @@ export default {
type: Object,
default: null,
},
deferSave: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -936,6 +1011,10 @@ export default {
reason: '',
},
adjustRows: [],
billingRuleDialog: {
visible: false,
rule: null,
},
attachmentFileTypes: [
'pdf',
'bmp',
@@ -1146,6 +1225,9 @@ export default {
this.changeRecords = [];
this.attachments = [];
this.selectedAttachmentRows = [];
this.adjustRows = [];
this.billingRuleDialog.visible = false;
this.billingRuleDialog.rule = null;
this.contractOptions = [];
this.contractDialog.visible = false;
this.contractDialog.expanded = false;
@@ -1421,6 +1503,7 @@ export default {
this.summaryFees.splice(index, 1);
},
recalculateSummaryRow(row) {
if (Number(row.manualFlag) !== 1) return;
row.settlementAmount = Number(row.originalAmount || 0) + Number(row.adjustAmount || 0);
},
buildSummaryFeesFromDetails() {
@@ -1548,6 +1631,13 @@ export default {
}
});
this.candidateDialog.confirming = true;
if (this.deferSave) {
this.buildSummaryFeesFromDetails();
this.candidateDialog.visible = false;
this.candidateDialog.confirming = false;
this.$message.success('结算明细添加成功,请点击保存提交');
return;
}
try {
const { data } = await save(this.buildSavePayload());
this.form.id = data?.data || this.form.id;
@@ -1588,6 +1678,10 @@ export default {
const isPersistedDetail =
row.id && sourceDetailId && String(row.id) !== String(sourceDetailId);
if (isPersistedDetail) return row;
if (this.deferSave) {
this.$message.warning('请先保存预结算单后再调整结算明细');
return null;
}
await this.$refs.formRef?.validate();
this.loading = true;
@@ -1621,11 +1715,97 @@ export default {
this.adjustRows = (data?.data || []).map(item => ({
...item,
feeItems: this.parseFeeItems(item.feeItemsJson),
billingRule: this.normalizeBillingRule(item),
}));
} finally {
this.adjustDialog.loading = false;
}
},
billingRuleName(row) {
return this.normalizeBillingRule(row).name;
},
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
);
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 ||
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,
transportMode: matchCondition.transportMode || matchCondition.transportType,
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 '-';
},
recalculateAdjustRow(row, changedField) {
if (changedField && changedField !== 'freight' && this.isFreightFeeItem(changedField)) {
row.freightAmount = Number(row.feeItems[changedField] || 0);
@@ -1797,6 +1977,25 @@ export default {
return {};
}
},
parseObject(value) {
if (!value) return {};
if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
if (typeof value !== 'string') return {};
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch (error) {
return {};
}
},
parseJsonValue(value) {
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch (error) {
return value;
}
},
formatMoney(value, currency = 'RMB') {
if (value === undefined || value === null || value === '') return '-';
const amount = Number(value);
@@ -1983,6 +2182,28 @@ export default {
margin - top : 16 px ;
}
& _ _billing - rule - cell {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
max - width : 100 % ;
}
& _ _billing - rule - icon {
flex : 0 0 auto ;
color : var ( -- el - color - primary ) ;
cursor : pointer ;
}
& _ _billing - rule - detail {
margin - top : 12 px ;
}
& _ _billing - rule - match - title {
margin - top : 20 px ;
}
. form - readonly {
min - height : 32 px ;
line - height : 32 px ;