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

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

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
+453 -126
View File
@@ -140,7 +140,8 @@
:step="0.01"
:controls="false"
style="width: 100%"
value-on-clear="0"
:value-on-clear="null"
placeholder="请输入申请付款金额"
/></el-form-item
></el-col>
<el-col :span="6"
@@ -241,11 +242,22 @@
<template #default="{ row }">{{ formatMoney(row.payableAmount) }}</template>
</el-table-column>
<el-table-column prop="paymentRatio" label="付款比例(%)" min-width="110">
<template #default="{ row }">{{ formatMoney(row.paymentRatio) }}</template>
<template #default="{ row }">
<span :class="{ 'payment-form-page__ratio-exceeded': row.ratioExceeded }">
{{ formatMoney(row.paymentRatio) }}
</span>
</template>
</el-table-column>
<el-table-column prop="appliedAmount" label="付款金额" min-width="130">
<template #default="{ row }">{{ formatMoney(row.appliedAmount) }}</template>
</el-table-column>
<el-table-column prop="ratioRemark" label="备注" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<span :class="{ 'payment-form-page__ratio-exceeded': row.ratioExceeded }">
{{ row.ratioRemark || '-' }}
</span>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card v-if="showInvoiceSection" title="发票信息">
@@ -409,7 +421,7 @@
<el-button v-if="!readonly" type="primary" plain @click="syncPaymentRecords"
>同步</el-button
>
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft()"
<el-button v-if="!readonly && canSave" type="primary" plain @click="handleSave"
>保存</el-button
>
<el-button
@@ -483,7 +495,7 @@
</el-form>
</el-card>
<el-tabs v-model="referenceTab" @tab-change="handleReferenceTabChange">
<el-tab-pane v-if="canSelectFormalSettlement" label="正式结算单" name="formal">
<el-tab-pane v-if="canSelectFormalSettlement" label="付款结算单" name="formal">
<el-table
v-loading="formalPage.loading"
:data="formalRows"
@@ -491,12 +503,19 @@
@selection-change="formalSelection = $event"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="formalSettlementNo" label="结算单号" />
<el-table-column prop="projectName" label="所属项目" />
<el-table-column prop="contractName" label="合同名称" />
<el-table-column prop="settlementAmount" label="结算金额">
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="150" />
<el-table-column prop="projectName" label="所属项目" min-width="140" />
<el-table-column prop="contractName" label="合同名称" min-width="160" />
<el-table-column prop="settlementAmount" label="结算金额" min-width="130">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column prop="payableAmount" label="可付款金额" min-width="130">
<template #default="{ row }">{{
row.payableAmount === undefined || row.payableAmount === null
? '-'
: formatMoney(row.payableAmount)
}}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectFormal(row)">选择</el-link>
@@ -515,7 +534,7 @@
/>
</div>
</el-tab-pane>
<el-tab-pane v-if="canSelectPreSettlement" label="预结算单" name="pre">
<el-tab-pane v-if="canSelectPreSettlement" label="预付结算单" name="pre">
<el-table
v-loading="prePage.loading"
:data="preRows"
@@ -523,12 +542,19 @@
@selection-change="preSelection = $event"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="preSettlementNo" label="结算单号" />
<el-table-column prop="projectName" label="所属项目" />
<el-table-column prop="contractName" label="合同名称" />
<el-table-column prop="settlementAmount" label="结算金额">
<el-table-column prop="preSettlementNo" label="结算单号" min-width="150" />
<el-table-column prop="projectName" label="所属项目" min-width="140" />
<el-table-column prop="contractName" label="合同名称" min-width="160" />
<el-table-column prop="settlementAmount" label="结算金额" min-width="130">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column prop="payableAmount" label="可付款金额" min-width="130">
<template #default="{ row }">{{
row.payableAmount === undefined || row.payableAmount === null
? '-'
: formatMoney(row.payableAmount)
}}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectPre(row)">选择</el-link>
@@ -562,22 +588,27 @@
</template>
</el-dialog>
<el-dialog v-model="billDialogVisible" title="选择汇票" width="80%" append-to-body>
<div class="payment-form-page__reference-search">
<el-form :model="billQuery" label-position="right" label-width="160px" @submit.prevent>
<div class="payment-form-page__reference-search-fields">
<el-form-item label="汇票筛选">
<el-input
v-model="billQuery.keyword"
clearable
placeholder="请输入汇票号、出票单位或收票单位"
@keyup.enter="handleBillSearch"
/>
</el-form-item>
</div>
<div class="payment-form-page__reference-search-actions">
<el-button type="primary" @click="handleBillSearch">搜索</el-button>
<el-button @click="resetBillSearch">清空</el-button>
</div>
<div class="payment-form-page__reference-search payment-form-page__bill-search">
<el-form
:model="billQuery"
inline
label-position="right"
label-width="88px"
@submit.prevent
>
<el-form-item label="汇票筛选">
<el-input
v-model="billQuery.keyword"
clearable
style="width: 320px"
placeholder="请输入汇票号、出票单位或收票单位"
@keyup.enter="handleBillSearch"
/>
</el-form-item>
<el-form-item class="payment-form-page__bill-search-actions">
<el-button type="primary" @click="handleBillSearch">查询</el-button>
<el-button @click="resetBillSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="billPage.loading" :data="billRows" border>
@@ -652,7 +683,7 @@
</template>
<script>
import { ElImageViewer } from 'element-plus';
import { ElImageViewer, ElMessage } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
@@ -814,8 +845,12 @@ export default {
contractSignatureFileAvailable: false,
contractPaymentRatioLimit: null,
contractPaymentRatioExceeded: false,
/** 合同付款比例设置(按第1笔、第2笔…顺序) */
contractPaymentRatioRows: [],
payeeCustomerLevel: '',
attachmentRuleLoading: false,
// 最近一次从结算单同步过来的附件 URL,切换结算单时用于整批替换
linkedSettlementAttachmentUrls: [],
attachmentImagePreviewVisible: false,
attachmentImagePreviewUrls: [],
attachmentImagePreviewIndex: 0,
@@ -860,7 +895,7 @@ export default {
billLedgerId: [{ validator: this.validateBillLedger, trigger: 'change' }],
settlementAmount: [{ validator: this.validateSettlementAmount, trigger: 'change' }],
appliedAmount: [
{ required: true, validator: this.validateAppliedAmount, trigger: 'change' },
{ required: true, validator: this.validateAppliedAmount, trigger: ['change', 'blur'] },
],
},
};
@@ -905,8 +940,8 @@ export default {
referenceDialogTitle() {
if (this.form.paymentType === 'project_advance') return '选择项目合同';
if (this.canSelectPreSettlement && this.canSelectFormalSettlement) return '选择结算单';
if (this.canSelectPreSettlement) return '选择预结算单';
if (this.canSelectFormalSettlement) return '选择正式结算单';
if (this.canSelectPreSettlement) return '选择预付结算单';
if (this.canSelectFormalSettlement) return '选择付款结算单';
return '选择结算单';
},
billPayment() {
@@ -960,10 +995,8 @@ export default {
},
settlementRows() {
if (this.referenceRows.length) {
return this.referenceRows.map(item => ({
...item,
paymentRatio: this.form.paymentRatio,
appliedAmount:
return this.referenceRows.map(item => {
const appliedAmount =
item.fixedAppliedAmount === true
? Number(item.appliedAmount || 0)
: Number(
@@ -971,8 +1004,14 @@ export default {
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
100
).toFixed(2)
),
}));
);
return {
...item,
paymentRatio: this.form.paymentRatio,
appliedAmount,
...this.buildSettlementRatioHint(item),
};
});
}
if (!this.referenceLabel) return [];
return [
@@ -982,6 +1021,8 @@ export default {
payableAmount: this.form.payableAmount,
paymentRatio: this.form.paymentRatio,
appliedAmount: this.form.appliedAmount,
paymentSequence: 1,
...this.buildSettlementRatioHint({ paymentSequence: 1 }),
},
];
},
@@ -1071,7 +1112,7 @@ export default {
!this.form.preSettlementId &&
!this.form.preSettlementIds?.length
) {
callback(new Error('请选择预结算单'));
callback(new Error('请选择预付结算单'));
return;
}
if (
@@ -1102,14 +1143,18 @@ export default {
callback();
},
validateAppliedAmount(rule, value, callback) {
if (value === null || value === undefined || value === '') {
callback(new Error('请填写申请付款金额'));
return;
}
const amount = Number(value);
const payableAmount = Number(this.form.payableAmount || 0);
if (value === null || value === undefined || value === '' || !Number.isFinite(amount)) {
if (!Number.isFinite(amount)) {
callback(new Error('请填写申请付款金额'));
return;
}
if (amount <= 0) {
callback(new Error('申请付款金额必须大于0'));
callback(new Error('申请付款金额必须大于0,不能为0元'));
return;
}
if (
@@ -1132,10 +1177,23 @@ export default {
return;
}
const selected = this.billOptions.find(item => String(item.id) === String(value));
if (
selected &&
Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)
) {
if (!selected) {
callback();
return;
}
if (selected.billType && selected.billType !== 'received') {
callback(new Error('汇票付款仅可选择收票类型的汇票'));
return;
}
if (selected.maturityDate) {
const maturity = this.$dayjs(selected.maturityDate).startOf('day');
const today = this.$dayjs().startOf('day');
if (maturity.isValid() && maturity.isBefore(today)) {
callback(new Error('所选汇票已到期,不可选择'));
return;
}
}
if (Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)) {
callback(new Error('申请付款金额不能超过汇票可用余额'));
return;
}
@@ -1263,6 +1321,7 @@ export default {
await this.loadContractOptions(this.form.projectId);
}
await this.fillBillTypeFromContract(this.form.contractId);
await this.refreshSettlementPaymentSequences();
await this.loadAttachmentRuleData(await this.loadCurrentSettlementDetails());
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
} else {
@@ -1290,6 +1349,7 @@ export default {
this.form.billType = '';
this.contractPartyBName = '';
this.receiptAccountOptions = [];
this.resetContractPaymentRatioLimit();
return;
}
try {
@@ -1301,11 +1361,13 @@ export default {
this.form.payerName = partyA;
}
this.contractPartyBName = String(data?.partyB || data?.payeeName || '').trim();
this.applyContractPaymentRatioLimit(data || {});
await this.loadReceiptAccountOptions(autoSelectDefault);
} catch (error) {
this.form.billType = '';
this.contractPartyBName = '';
this.receiptAccountOptions = [];
this.resetContractPaymentRatioLimit();
}
},
async loadPaymentMethodOptions() {
@@ -1400,16 +1462,33 @@ export default {
if (!rows.length) return;
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
if (contractIds.size !== 1 || contractIds.has('')) {
this.$message.warning('所选正式结算单合同信息不一致,请返回后重新选择');
this.$message.warning('所选付款结算单合同信息不一致,请返回后重新选择');
return;
}
const first = rows[0];
const referenceRows = rows.map(item => ({
...this.createReferenceRow(item),
settlementNo: item.formalSettlementNo || item.settlementNo || '',
appliedAmount: Number(item.appliedAmount || 0),
fixedAppliedAmount: true,
}));
const amountResponses = await Promise.all(
rows.map(item => {
const referenceId = item.settlementId || item.id;
if (!this.isValidReferenceId(referenceId)) return Promise.resolve(null);
return api
.getReferenceAmount('settlement_payment', referenceId, this.form.id)
.catch(() => null);
})
);
const referenceRows = rows.map((item, index) => {
const amount = this.unwrapData(amountResponses[index]) || {};
return {
...this.createReferenceRow({
...item,
settlementAmount: amount.settlementAmount ?? item.settlementAmount,
payableAmount: amount.payableAmount ?? item.payableAmount,
paymentSequence: amount.paymentSequence,
}),
settlementNo: item.formalSettlementNo || item.settlementNo || '',
appliedAmount: Number(item.appliedAmount || 0),
fixedAppliedAmount: true,
};
});
const settlementAmount = referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0),
0
@@ -1503,21 +1582,76 @@ export default {
},
createReferenceRow(row) {
const settlementAmount = Number(row.settlementAmount || 0);
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
const isPre = Boolean(row.preSettlementNo || row.preSettlementId || row.referenceType === 'pre');
const paymentSequence = Number(row.paymentSequence);
// 可付款金额必须来自 referenceAmount 接口(已扣减占用中的付款申请);缺失时置 0,避免误用未扣减结算金额
const payableAmount =
row.payableAmount === undefined || row.payableAmount === null
? 0
: Number(row.payableAmount || 0);
return {
id: row.preSettlementId || row.settlementId || row.id,
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal',
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
payableAmount: Number(
(row.payableAmount === undefined || row.payableAmount === null
? Math.max(0, settlementAmount - appliedAmount)
: Number(row.payableAmount || 0)
).toFixed(2)
),
payableAmount: Number(Math.max(0, payableAmount).toFixed(2)),
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
};
},
/**
* 按合同付款比例配置,判断当前付款次序是否超上限。
* @param {Object} row 结算信息行(含 paymentSequence)
*/
buildSettlementRatioHint(row = {}) {
const sequence = Number(row.paymentSequence || 1);
const paymentRatio = Number(this.form.paymentRatio);
const ratioConfig = this.contractPaymentRatioRows[sequence - 1];
const ratioLimitValue = ratioConfig?.ratioLimit;
const ratioLimit = Number(ratioLimitValue);
const hasLimit =
ratioLimitValue !== null &&
ratioLimitValue !== undefined &&
String(ratioLimitValue).trim() !== '' &&
Number.isFinite(ratioLimit);
const hasPaymentRatio =
this.form.paymentRatio !== null &&
this.form.paymentRatio !== undefined &&
String(this.form.paymentRatio).trim() !== '' &&
Number.isFinite(paymentRatio);
const ratioExceeded = hasLimit && hasPaymentRatio && paymentRatio > ratioLimit;
return {
paymentSequence: Number.isFinite(sequence) && sequence > 0 ? sequence : 1,
contractRatioLimit: hasLimit ? ratioLimit : null,
ratioExceeded,
ratioRemark: ratioExceeded
? `合同约定第${sequence}笔付款比例上限为${Number(ratioLimit).toFixed(2)}%`
: '',
};
},
/** 为结算信息行回填付款次序(第几次付款) */
async refreshSettlementPaymentSequences() {
if (!this.isSettlementRelatedType || !this.referenceRows.length) {
return;
}
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
const amountResponses = await Promise.all(
this.referenceRows.map(item => {
if (!this.isValidReferenceId(item.id)) {
return Promise.resolve(null);
}
return api.getReferenceAmount(paymentType, item.id, this.form.id).catch(() => null);
})
);
this.referenceRows = this.referenceRows.map((item, index) => {
const amount = this.unwrapData(amountResponses[index]) || {};
const paymentSequence = Number(amount.paymentSequence || item.paymentSequence || 1);
return {
...item,
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
};
});
},
createInvoiceRows(invoices = [], settlementNo = '') {
return (invoices || []).map(invoice => ({
settlementNo,
@@ -1568,29 +1702,41 @@ export default {
if (!rows.length) return;
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
if (contractIds.size > 1 || contractIds.has('')) {
this.$message.warning('所选预结算单合同信息不一致,请返回后重新选择');
this.$message.warning('所选预付结算单合同信息不一致,请返回后重新选择');
return;
}
const first = rows[0];
this.form.invoices = [];
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
const amountResponses = await Promise.all(
rows.map(item => {
const referenceId = item.preSettlementId || item.id;
if (!this.isValidReferenceId(referenceId)) return Promise.resolve(null);
return api.getReferenceAmount(paymentType, referenceId, this.form.id).catch(() => null);
})
);
this.referenceRows = rows.map((item, index) => {
const amount = this.unwrapData(amountResponses[index]) || {};
return this.createReferenceRow({
...item,
referenceType: 'pre',
settlementAmount: amount.settlementAmount ?? item.settlementAmount,
payableAmount: amount.payableAmount,
try {
const amountResponses = await Promise.all(
rows.map(item => {
const referenceId = item.preSettlementId || item.id;
if (!this.isValidReferenceId(referenceId)) {
return Promise.reject(new Error('结算单无效'));
}
return api.getReferenceAmount(paymentType, referenceId, this.form.id);
})
);
this.referenceRows = rows.map((item, index) => {
const amount = this.unwrapData(amountResponses[index]) || {};
const settlementAmount = Number(amount.settlementAmount ?? item.settlementAmount ?? 0);
if (amount.payableAmount === undefined || amount.payableAmount === null) {
throw new Error('可付款金额获取失败,请重试');
}
return this.createReferenceRow({
...item,
referenceType: 'pre',
settlementAmount,
payableAmount: Number(amount.payableAmount || 0),
paymentSequence: amount.paymentSequence,
});
});
});
} catch (error) {
this.$message.error(error?.message || '可付款金额获取失败,请重试');
return;
}
const settlementAmount = this.referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0),
0
@@ -1600,7 +1746,7 @@ export default {
0
);
const paymentRatio = Number(this.form.paymentRatio || 0);
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
// 尾款付款场景下选择预付结算单时保留尾款付款类型,不强制改为进度预付
this.amountSyncing = true;
Object.assign(this.form, {
paymentType,
@@ -1625,7 +1771,8 @@ export default {
settlementAmount: Number(settlementAmount.toFixed(2)),
payableAmount: Number(payableAmount.toFixed(2)),
billType: '',
appliedAmount: Number(((settlementAmount * paymentRatio) / 100).toFixed(2)),
// 申请金额按可付款金额×比例,避免未扣减已申请额度
appliedAmount: Number(((payableAmount * paymentRatio) / 100).toFixed(2)),
});
this.paymentRecords = [];
this.clearReceiptAccount();
@@ -1827,7 +1974,7 @@ export default {
);
return typeMatched || this.matchAttachmentMaterialByFileName(file, materials);
},
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null) {
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null, sourceKey = '') {
const existingUrls = new Set(
(this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean)
);
@@ -1848,26 +1995,95 @@ export default {
attachmentType: MATERIAL_TYPE_MAP[material.key],
description: file.description || `自动取自${sourceName}`,
sourceImported: true,
sourceKey: sourceKey || undefined,
};
});
if (imported.length) this.form.attachments = [...this.form.attachments, ...imported];
return imported;
},
/** 移除指定来源的自动关联附件(切换结算单/合同时整批替换) */
removeAutomaticAttachments(sourceKey, sourceName = '') {
const previousUrls = new Set(
sourceKey === 'settlement' ? this.linkedSettlementAttachmentUrls || [] : []
);
this.form.attachments = (this.form.attachments || []).filter(file => {
const url = this.attachmentFileUrl(file);
if (sourceKey && file.sourceKey === sourceKey) return false;
if (
sourceName &&
file.sourceImported &&
String(file.description || '').includes(`自动取自${sourceName}`)
) {
return false;
}
if (previousUrls.size && url && previousUrls.has(url)) return false;
return true;
});
if (sourceKey === 'settlement') {
this.linkedSettlementAttachmentUrls = [];
}
},
/** 按当前结算单附件整批同步:先清旧关联,再写入新结算单上传的附件 */
syncSettlementAttachments(settlementRows = []) {
const settlementMaterials = [
ATTACHMENT_MATERIALS.weighingSlip,
ATTACHMENT_MATERIALS.entrustOrder,
ATTACHMENT_MATERIALS.settlement,
];
const settlementFiles = (settlementRows || []).flatMap(row => this.parse(row.attachmentsJson));
const nextUrlSet = new Set(
settlementFiles.map(file => this.attachmentFileUrl(file)).filter(Boolean)
);
// 编辑回显时草稿里已有结算单附件但无 sourceKey:用 URL 交集补齐待替换清单
if (!(this.linkedSettlementAttachmentUrls || []).length && nextUrlSet.size) {
this.linkedSettlementAttachmentUrls = (this.form.attachments || [])
.map(file => this.attachmentFileUrl(file))
.filter(url => url && nextUrlSet.has(url));
}
this.removeAutomaticAttachments('settlement', '结算单');
const imported = this.mergeAutomaticAttachments(
settlementFiles,
settlementMaterials,
'结算单',
null,
'settlement'
);
this.linkedSettlementAttachmentUrls = imported
.map(file => this.attachmentFileUrl(file))
.filter(Boolean);
},
async loadCurrentSettlementDetails() {
const preIds = [
...(this.form.preSettlementIds || []),
this.form.preSettlementId,
...(this.referenceRows || [])
.filter(item => item.referenceType === 'pre')
.map(item => item.id),
]
.map(id => String(id ?? '').trim())
.filter(id => this.isValidReferenceId(id));
const formalIds = [
...(this.form.settlementIds || []),
this.form.settlementId,
...(this.referenceRows || [])
.filter(item => item.referenceType !== 'pre')
.map(item => item.id),
]
.map(id => String(id ?? '').trim())
.filter(id => this.isValidReferenceId(id));
const uniquePreIds = [...new Set(preIds)];
const uniqueFormalIds = [...new Set(formalIds)];
if (
this.form.paymentType === 'progress_advance' &&
this.isValidReferenceId(this.form.preSettlementId)
(this.form.paymentType === 'progress_advance' ||
this.form.paymentType === 'settlement_payment') &&
uniquePreIds.length
) {
const response = await preApi.getDetail(this.form.preSettlementId);
const detail = this.unwrapData(response);
return detail?.id ? [detail] : [];
const responses = await Promise.all(uniquePreIds.map(id => preApi.getDetail(id)));
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
}
if (
this.form.paymentType === 'settlement_payment' &&
this.isValidReferenceId(this.form.settlementId)
) {
const response = await formalApi.getDetail(this.form.settlementId);
const detail = this.unwrapData(response);
return detail?.id ? [detail] : [];
if (this.form.paymentType === 'settlement_payment' && uniqueFormalIds.length) {
const responses = await Promise.all(uniqueFormalIds.map(id => formalApi.getDetail(id)));
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
}
return [];
},
@@ -1877,32 +2093,29 @@ export default {
this.contractSignatureFileAvailable = false;
this.resetContractPaymentRatioLimit();
try {
const settlementMaterials = [
ATTACHMENT_MATERIALS.weighingSlip,
ATTACHMENT_MATERIALS.entrustOrder,
ATTACHMENT_MATERIALS.settlement,
];
const settlementFiles = (settlementRows || []).flatMap(row =>
this.parse(row.attachmentsJson)
);
this.mergeAutomaticAttachments(settlementFiles, settlementMaterials, '结算单');
// 切换结算单时,附件区同步替换为当前结算单已上传附件
this.syncSettlementAttachments(settlementRows);
if (!this.form.contractId) {
this.firstContractPayment = false;
this.removeAutomaticAttachments('contract', '合同');
return;
}
const contractResponse = await getContractDetail(this.form.contractId);
const contract = this.unwrapData(contractResponse) || {};
// 结算信息需用合同全部付款比例配置判断第几笔是否超限
this.applyContractPaymentRatioLimit(contract);
if (this.form.paymentType === 'project_advance') {
this.applyContractPaymentRatioLimit(contract);
const contractFiles = this.parse(contract.contractFileJson);
this.contractSignatureFileAvailable = contractFiles.some(file =>
Boolean(this.attachmentFileUrl(file))
);
this.removeAutomaticAttachments('contract', '合同');
this.mergeAutomaticAttachments(
contractFiles,
[ATTACHMENT_MATERIALS.contract],
'合同',
ATTACHMENT_MATERIALS.contract
ATTACHMENT_MATERIALS.contract,
'contract'
);
return;
}
@@ -1916,12 +2129,14 @@ export default {
String(item.id || '') !== String(this.form.id || '') &&
item.approvalStatus !== 'voided'
);
this.removeAutomaticAttachments('contract', '合同');
if (this.firstContractPayment) {
this.mergeAutomaticAttachments(
this.parse(contract.contractFileJson),
[ATTACHMENT_MATERIALS.contract],
'合同',
ATTACHMENT_MATERIALS.contract
ATTACHMENT_MATERIALS.contract,
'contract'
);
}
} catch (error) {
@@ -1933,9 +2148,12 @@ export default {
resetContractPaymentRatioLimit() {
this.contractPaymentRatioLimit = null;
this.contractPaymentRatioExceeded = false;
this.contractPaymentRatioRows = [];
},
applyContractPaymentRatioLimit(contract = {}) {
const [firstRatio] = this.parse(contract.paymentRatioJson);
const ratioRows = this.parse(contract.paymentRatioJson).filter(item => item && typeof item === 'object');
this.contractPaymentRatioRows = ratioRows;
const [firstRatio] = ratioRows;
const ratioLimitValue = firstRatio?.ratioLimit;
const ratioLimit = Number(ratioLimitValue);
this.contractPaymentRatioLimit =
@@ -2279,6 +2497,18 @@ export default {
this.loadBillPage();
},
selectBill(row) {
if (row?.billType && row.billType !== 'received') {
ElMessage.warning('汇票付款仅可选择收票类型的汇票');
return;
}
if (row?.maturityDate) {
const maturity = this.$dayjs(row.maturityDate).startOf('day');
const today = this.$dayjs().startOf('day');
if (maturity.isValid() && maturity.isBefore(today)) {
ElMessage.warning('所选汇票已到期,不可选择');
return;
}
}
this.form.billLedgerId = row.id;
this.form.billNo = row.billNo || '';
this.billOptions = [row];
@@ -2296,6 +2526,33 @@ export default {
contractName: String(contractName || '').trim() || undefined,
};
},
async fillReferencePayableAmounts(rows, paymentType) {
if (!rows?.length) return rows || [];
const amountResponses = await Promise.all(
rows.map(row => {
const referenceId = row.id;
if (!this.isValidReferenceId(referenceId)) {
return Promise.resolve(null);
}
return api
.getReferenceAmount(paymentType, referenceId, this.form.id)
.catch(() => null);
})
);
return rows.map((row, index) => {
const amount = this.unwrapData(amountResponses[index]) || {};
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
const payableAmount =
amount.payableAmount === undefined || amount.payableAmount === null
? null
: Number(amount.payableAmount || 0);
return {
...row,
settlementAmount,
payableAmount,
};
});
},
async loadFormalReferences() {
this.formalPage.loading = true;
try {
@@ -2305,7 +2562,8 @@ export default {
this.referenceParams('formal')
);
const data = this.unwrapData(response) || {};
this.formalRows = data.records || [];
const records = data.records || [];
this.formalRows = await this.fillReferencePayableAmounts(records, 'settlement_payment');
this.formalPage.total = Number(data.total || 0);
} finally {
this.formalPage.loading = false;
@@ -2320,7 +2578,10 @@ export default {
this.referenceParams('pre')
);
const data = this.unwrapData(response) || {};
this.preRows = data.records || [];
const records = data.records || [];
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
this.preRows = await this.fillReferencePayableAmounts(records, paymentType);
this.prePage.total = Number(data.total || 0);
} finally {
this.prePage.loading = false;
@@ -2413,7 +2674,7 @@ export default {
}
}
if (!id) {
this.$message.warning(isPre ? '未找到对应的预结算单' : '未找到对应的正式结算单');
this.$message.warning(isPre ? '未找到对应的预付结算单' : '未找到对应的付款结算单');
return;
}
this.$router.push({
@@ -2421,7 +2682,7 @@ export default {
query: {
mode: 'view',
id,
name: isPre ? '查看预结算' : '查看正式结算',
name: isPre ? '查看预付结算单' : '查看付款结算单',
},
});
},
@@ -2463,7 +2724,14 @@ export default {
const amount = this.unwrapData(amountResponse) || {};
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
const payableAmount = Number(amount.payableAmount ?? 0);
this.referenceRows = [this.createReferenceRow({ ...row, settlementAmount, payableAmount })];
this.referenceRows = [
this.createReferenceRow({
...row,
settlementAmount,
payableAmount,
paymentSequence: amount.paymentSequence,
}),
];
this.clearReceiptAccount();
Object.assign(this.form, {
paymentType: 'settlement_payment',
@@ -2508,14 +2776,17 @@ export default {
const rows = this.formalSelection || [];
if (!rows.length) return;
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
this.$message.warning('所选正式结算单必须属于同一合同');
this.$message.warning('所选付款结算单必须属于同一合同');
return;
}
const validRows = rows.filter(row => this.isValidReferenceId(row.id));
const details = await Promise.all(validRows.map(row => formalApi.getDetail(row.id)));
await this.applyTransferredFormalSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
const detailRows = details.map((response, index) => ({
...validRows[index],
...this.unwrapData(response),
}));
await this.applyTransferredFormalSettlements(detailRows);
await this.loadAttachmentRuleData(detailRows);
this.referenceVisible = false;
},
async selectPre(row) {
@@ -2533,7 +2804,13 @@ export default {
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
const payableAmount = Number(amount.payableAmount ?? 0);
this.referenceRows = [
this.createReferenceRow({ ...row, settlementAmount, payableAmount, referenceType: 'pre' }),
this.createReferenceRow({
...row,
settlementAmount,
payableAmount,
referenceType: 'pre',
paymentSequence: amount.paymentSequence,
}),
];
this.form.invoices = [];
this.paymentRecords = [];
@@ -2572,14 +2849,17 @@ export default {
const rows = this.preSelection || [];
if (!rows.length) return;
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
this.$message.warning('所选预结算单必须属于同一合同');
this.$message.warning('所选预付结算单必须属于同一合同');
return;
}
const validRows = rows.filter(row => this.isValidReferenceId(row.id));
const details = await Promise.all(validRows.map(row => preApi.getDetail(row.id)));
await this.applyTransferredPreSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
const detailRows = details.map((response, index) => ({
...validRows[index],
...this.unwrapData(response),
}));
await this.applyTransferredPreSettlements(detailRows);
await this.loadAttachmentRuleData(detailRows);
this.referenceVisible = false;
},
normalizeAttachments(files) {
@@ -2722,30 +3002,60 @@ export default {
})),
};
},
/** 顶部「保存」:保存成功后关闭表单页并回到列表 */
async handleSave() {
await this.saveDraft({ showSuccess: true, closePage: true });
},
clearMessages() {
ElMessage.closeAll();
this.$message?.closeAll?.();
},
async saveDraft(options = {}) {
const showSuccess = options.showSuccess !== false;
// 仅显式 showSuccess:true 时提示;提交前静默保存默认不弹「保存成功」
const showSuccess = options.showSuccess === true;
const closePage = options.closePage === true;
await this.$refs.formRef.validate();
const appliedAmount = Number(this.form.appliedAmount);
if (
this.form.appliedAmount === null ||
this.form.appliedAmount === undefined ||
this.form.appliedAmount === '' ||
!Number.isFinite(appliedAmount) ||
appliedAmount <= 0
) {
this.clearMessages();
ElMessage.warning('申请付款金额必填且必须大于0');
return false;
}
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.$message.warning('请选择所属项目');
this.clearMessages();
ElMessage.warning('请选择所属项目');
return false;
}
try {
this.validateInvoices();
this.validatePaymentRecords();
} catch (error) {
this.$message.warning(error.message);
this.clearMessages();
ElMessage.warning(error.message);
return false;
}
const data = this.unwrapData(await api.save(this.payload()));
this.form.id = data;
if (showSuccess) {
this.$message.success('保存成功');
this.clearMessages();
ElMessage.success('保存成功');
}
if (closePage) {
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
this.goBack();
}
return true;
},
async submitForm() {
// 提交前静默保存,避免同时弹出「保存成功」与材料缺失 toast
const saved = await this.saveDraft({ showSuccess: false });
// 提交前静默保存;不校验/提示附件未上传;全程只保留一条「提交成功」
this.clearMessages();
const saved = await this.saveDraft({ showSuccess: false, closePage: false });
if (!saved) return;
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
@@ -2756,8 +3066,9 @@ export default {
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.$message.success('提交成功');
// 通知列表页(keep-alive)重新拉取数据
this.clearMessages();
ElMessage.success('提交成功');
// 通知列表页(keep-alive)重新拉取列表
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
this.goBack();
},
@@ -2820,6 +3131,19 @@ export default {
display: flex;
justify-content: flex-end;
}
.payment-form-page__bill-search :deep(.el-form--inline) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 8px;
}
.payment-form-page__bill-search :deep(.el-form-item) {
margin-right: 0;
margin-bottom: 8px;
}
.payment-form-page__bill-search-actions {
margin-left: 0;
}
.payment-form-page__reference-pagination {
margin-top: 12px;
}
@@ -2854,6 +3178,9 @@ export default {
font-size: 13px;
font-weight: 400;
}
.payment-form-page__ratio-exceeded {
color: var(--el-color-danger);
}
.payment-form-page__attachment-checklist {
display: flex;
flex-wrap: wrap;