Files
tms-erp-web/src/views/payment/payment-application-form.vue
T

3569 lines
134 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="payment-form-page">
<div class="archive-page-form__title">
{{ readonly ? '查看付款申请' : recordId ? '编辑付款申请' : '新增付款申请' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="payment-form-page__form"
>
<section-card title="付款基本信息">
<el-row :gutter="24">
<el-col :span="6"
><el-form-item label="单据号"
><el-input
v-model="form.paymentNo"
disabled
placeholder="系统自动生成" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="付款类型" prop="paymentType"
><el-select
v-model="form.paymentType"
:disabled="readonly || paymentTypeLocked"
clearable
placeholder="请选择付款类型"
@change="handleTypeChange"
><el-option
v-for="item in paymentTypeOptions"
:key="item.value"
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="isSettlementRelatedType" :span="6"
><el-form-item label="结算单号" prop="referenceId">
<div class="payment-form-page__settlement-nos">
<el-tooltip
:content="referenceLabel"
placement="top"
:disabled="!referenceLabel || referenceSettlementCount < 2"
>
<el-input
:model-value="referenceDisplayText"
readonly
:disabled="readonly"
placeholder="请输入"
class="payment-form-page__settlement-input"
@click="!readonly && openReference()"
/>
</el-tooltip>
<el-button
v-if="!readonly"
class="payment-form-page__settlement-select-btn"
@click="openReference"
>
选择
</el-button>
</div>
</el-form-item></el-col
>
<el-col :span="6">
<el-form-item label="所属项目">
<el-select
v-if="form.paymentType === 'project_advance'"
v-model="form.projectId"
:disabled="readonly"
filterable
clearable
:loading="projectLoading"
placeholder="请选择项目"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName || item.projectShortName || item.projectCode"
:value="item.id"
/>
</el-select>
<el-input v-else v-model="form.projectName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同名称">
<el-select
v-if="form.paymentType === 'project_advance'"
v-model="form.contractId"
:disabled="readonly || !form.projectId"
filterable
clearable
:loading="contractLoading"
placeholder="请先选择项目"
@change="handleAdvanceContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="formatContractOptionLabel(item)"
:value="item.id"
/>
</el-select>
<el-input v-else v-model="form.contractName" disabled />
</el-form-item>
</el-col>
<el-col v-if="isSettlementRelatedType" :span="6"
><el-form-item label="结算金额" prop="settlementAmount"
><el-input :model-value="formatMoney(form.settlementAmount)" disabled /></el-form-item
></el-col>
<el-col v-if="isSettlementRelatedType" :span="6"
><el-form-item label="可付款金额"
><el-input :model-value="formatMoney(form.payableAmount)" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="票款类型"
><el-input v-model="form.billType" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="付款比例(%)" prop="paymentRatio"
><div class="payment-form-page__percentage-input">
<el-input-number
v-model="form.paymentRatio"
:disabled="readonly"
:min="0"
:max="100"
:precision="2"
:controls="false"
/><span class="payment-form-page__percentage-append">%</span>
</div></el-form-item
></el-col
>
<el-col :span="6"
><el-form-item label="申请付款金额" prop="appliedAmount"
><el-input-number
v-model="form.appliedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
style="width: 100%"
:value-on-clear="null"
placeholder="请输入申请付款金额"
/></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="付款方式" prop="paymentMethod"
><el-select
v-model="form.paymentMethod"
:disabled="readonly"
@change="handlePaymentMethodChange"
><el-option
v-for="item in paymentMethodOptions"
:key="item.value"
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="billPayment" :span="6"
><el-form-item label="汇票单号" prop="billLedgerId"
><el-input
v-model="form.billNo"
readonly
:disabled="readonly"
placeholder="请选择可用汇票"
><template #append
><el-button :disabled="readonly" @click="openBillDialog">选择</el-button></template
></el-input></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="付款方"
><el-input v-model="form.payerName" disabled /></el-form-item
></el-col>
<el-col :span="6">
<el-form-item label="收款方" prop="receiptAccountId">
<el-input v-if="readonly" v-model="form.payeeName" disabled />
<el-select
v-else
v-model="form.receiptAccountId"
:loading="receiptAccountLoading"
:disabled="!contractPartyBName"
filterable
clearable
placeholder="请选择收款方"
no-data-text="合同乙方客商未维护可用收款信息"
@change="handlePayeeReceiptChange"
>
<el-option
v-for="item in receiptAccountOptions"
:key="item.id"
:label="payeeReceiptLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6"
><el-form-item label="收款账号"
><el-input v-model="form.bankAccount" disabled placeholder="选择收款方后自动带出" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="开户银行"
><el-input v-model="form.bankName" disabled placeholder="选择收款方后自动带出" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="申请人"
><el-input v-model="form.applicantName" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="申请日期"
><el-input v-model="form.applyDate" disabled /></el-form-item
></el-col>
<el-col :span="24"
><el-form-item label="备注"
><el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
maxlength="200"
show-word-limit /></el-form-item
></el-col>
</el-row>
<!-- 风险客商提示(基本信息备注后,同合同新增页):收款方外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
<div v-if="payeeRiskText" class="payment-form-page__risk-tip">
<div class="payment-form-page__risk-tip-title">风险客商提示:</div>
<div class="payment-form-page__risk-tip-content">{{ payeeRiskText }}</div>
</div>
</section-card>
<section-card v-if="isSettlementRelatedType" title="结算信息">
<el-table :data="settlementRows" border
><el-table-column type="index" label="序号" width="70" />
<el-table-column prop="settlementNo" label="结算单号" min-width="150">
<template #default="{ row }">
<el-link
v-if="row.settlementNo"
type="primary"
@click="openSettlementDetail(row)"
>
{{ row.settlementNo }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<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 }">{{ formatMoney(row.payableAmount) }}</template>
</el-table-column>
<el-table-column prop="paymentRatio" label="付款比例(%)" min-width="160">
<template #default="{ row }">
<el-input-number
v-if="!readonly"
:model-value="row.paymentRatio"
:min="0"
:max="rowPaymentRatioMax(row)"
:precision="2"
:controls="false"
:value-on-clear="null"
style="width: 100%"
@update:model-value="value => handleRowPaymentRatioChange(row, value)"
/>
<span v-else :class="{ 'payment-form-page__ratio-exceeded': row.ratioExceeded }">
{{ formatMoney(row.paymentRatio) }}
</span>
</template>
</el-table-column>
<el-table-column prop="appliedAmount" label="付款金额" min-width="160">
<template #default="{ row }">
<el-input-number
v-if="!readonly"
:model-value="row.appliedAmount"
:min="0"
:max="Number(row.payableAmount || 0)"
:precision="2"
:controls="false"
:value-on-clear="null"
style="width: 100%"
@update:model-value="value => handleRowAppliedAmountChange(row, value)"
/>
<span v-else>{{ formatMoney(row.appliedAmount) }}</span>
</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="发票信息">
<el-table :data="form.invoices" border>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="settlementNo" label="结算单号" min-width="120">
<template #default="{ row }">
<el-link
v-if="row.settlementNo"
type="primary"
@click="openSettlementDetail({ settlementNo: row.settlementNo })"
>
{{ row.settlementNo }}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="invoiceNo" label="发票号" min-width="140" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="150" />
<el-table-column prop="invoiceType" label="发票类型" min-width="130" />
<el-table-column label="税率" min-width="100">
<template #default="{ row }">{{ Number(row.taxRate || 0) }}%</template>
</el-table-column>
<el-table-column label="发票金额(含税)" min-width="150">
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
</el-table-column>
<el-table-column label="匹配金额(含税)" min-width="150">
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row, $index }">
<div class="payment-form-page__invoice-actions">
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)"
>查看</el-link
>
<el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="downloadInvoiceAttachment(row)"
>下载</el-link
>
<el-link
v-if="!readonly"
type="danger"
@click="form.invoices.splice($index, 1)"
>
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="付款记录">
<el-table :data="paymentRecords" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
label="付款金额"
min-width="160"
><template #default="{ row }">{{
formatMoney(row.paidAmount)
}}</template></el-table-column
><el-table-column prop="paidDate" label="付款日期" min-width="150" /><el-table-column
prop="paymentNo"
label="付款单号"
min-width="160" /><el-table-column
prop="kingdeeBillNo"
label="付款凭证"
min-width="160"
/></el-table>
</section-card>
<section-card>
<template #title>
<span>附件</span>
<span v-if="missingAttachmentMaterials.length" class="payment-form-page__missing-text">
未上传:{{ missingAttachmentMaterials.map(item => item.label).join('、') }}
</span>
</template>
<div v-loading="attachmentRuleLoading" class="payment-form-page__attachment-checklist">
<el-button
class="payment-form-page__attachment-download"
type="primary"
plain
:disabled="!downloadableAttachments.length"
@click="handleAttachmentBatchDownload"
>
批量下载
</el-button>
</div>
<el-table :data="form.attachments" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
label="附件类型"
width="160"
><template #default="{ row }"
><el-select v-model="row.attachmentType" :disabled="readonly"
><el-option label="磅单" value="weighing_slip" /><el-option
label="委托单"
value="entrust_order" /><el-option label="结算单" value="settlement" /><el-option
label="合同签章文件"
value="contract" /><el-option
label="特批附件"
value="special_approval" /><el-option
label="其他"
value="other" /></el-select></template></el-table-column
><el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
><template #default="{ row }"
><span
class="attachment-file-name"
:class="{ 'is-disabled': !attachmentFileUrl(row) }"
role="button"
tabindex="0"
@click="attachmentFileUrl(row) && previewAttachment(row)"
@keydown.enter="attachmentFileUrl(row) && previewAttachment(row)"
@keydown.space.prevent="attachmentFileUrl(row) && previewAttachment(row)"
>{{ attachmentFileName(row) || '-' }}</span
></template
></el-table-column
><el-table-column label="附件描述" min-width="220"
><template #default="{ row }"
><el-input
v-model="row.description"
:disabled="readonly"
maxlength="200" /></template></el-table-column
><el-table-column label="文件大小" width="120"
><template #default="{ row }">{{
formatAttachmentSize(row)
}}</template></el-table-column
><el-table-column prop="uploadUserName" label="上传人" width="130" /><el-table-column
prop="uploadTime"
label="上传时间"
width="170"
/><el-table-column v-if="!readonly" label="操作" width="100"
><template #default="{ $index }"
><el-link type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
></template
></el-table-column
></el-table
>
<div class="payment-form-page__attachment-upload">
<vehicle-attachment-upload
v-model="form.attachments"
:readonly="readonly"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
@change="normalizeAttachments"
/>
</div>
</section-card>
<div class="payment-form-page__actions">
<el-button v-if="!readonly" type="primary" plain @click="syncPaymentRecords"
>同步</el-button
>
<el-button v-if="!readonly && canSave" type="primary" plain @click="handleSave"
>保存</el-button
>
<el-button
v-if="!readonly && canSave && hasPermission('payment_application_submit')"
type="primary"
@click="submitForm"
>提交</el-button
>
<el-button v-if="!isPublicViewPage" @click="goBack">返回</el-button>
</div>
</el-form>
<el-dialog
v-model="referenceVisible"
:title="referenceDialogTitle"
width="80%"
><el-table
v-if="form.paymentType === 'project_advance'"
:data="contractRows"
border
@row-click="selectContract"
><el-table-column prop="projectName" label="所属项目" /><el-table-column
prop="contractNo"
label="合同编号"
/><el-table-column prop="contractName" label="合同名称" /><el-table-column
prop="deptName"
label="所属组织"
/><el-table-column label="操作" width="100"
><template #default="{ row }"
><el-link type="primary" @click.stop="selectContract(row)">选择</el-link></template
></el-table-column
></el-table
>
<div v-else>
<el-card class="payment-form-page__reference-search">
<el-form
:model="referenceQuery"
label-position="right"
label-width="160px"
@submit.prevent
>
<div class="payment-form-page__reference-search-fields">
<el-form-item label="结算单号">
<el-input
v-model="referenceQuery.settlementNo"
clearable
placeholder="请输入结算单号"
@keyup.enter="handleReferenceSearch"
/>
</el-form-item>
<el-form-item label="项目">
<el-input
v-model="referenceQuery.projectName"
clearable
placeholder="请输入项目"
@keyup.enter="handleReferenceSearch"
/>
</el-form-item>
<el-form-item label="合同名称">
<el-input
v-model="referenceQuery.contractName"
clearable
placeholder="请输入合同名称"
@keyup.enter="handleReferenceSearch"
/>
</el-form-item>
</div>
<div class="payment-form-page__reference-search-actions">
<el-button type="primary" @click="handleReferenceSearch">搜索</el-button>
<el-button @click="resetReferenceSearch">清空</el-button>
</div>
</el-form>
</el-card>
<el-tabs v-model="referenceTab" @tab-change="handleReferenceTabChange">
<el-tab-pane v-if="canSelectFormalSettlement" label="付款结算单" name="formal">
<el-table
v-loading="formalPage.loading"
:data="formalRows"
border
@selection-change="formalSelection = $event"
>
<el-table-column type="selection" width="50" />
<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>
</template>
</el-table-column>
</el-table>
<div class="payment-form-page__reference-pagination">
<el-pagination
v-model:current-page="formalPage.current"
v-model:page-size="formalPage.size"
:total="formalPage.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadFormalReferences"
@size-change="handleFormalSizeChange"
/>
</div>
</el-tab-pane>
<el-tab-pane v-if="canSelectPreSettlement" label="预付结算单" name="pre">
<el-table
v-loading="prePage.loading"
:data="preRows"
border
@selection-change="preSelection = $event"
>
<el-table-column type="selection" width="50" />
<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>
</template>
</el-table-column>
</el-table>
<div class="payment-form-page__reference-pagination">
<el-pagination
v-model:current-page="prePage.current"
v-model:page-size="prePage.size"
:total="prePage.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadPreReferences"
@size-change="handlePreSizeChange"
/>
</div>
</el-tab-pane>
</el-tabs></div
>
<template v-if="isSettlementRelatedType" #footer>
<div class="payment-form-page__reference-footer">
<el-button @click="referenceVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!referenceSelection.length"
@click="confirmReferenceSelection"
>确定</el-button
>
</div>
</template>
</el-dialog>
<el-dialog v-model="billDialogVisible" title="选择汇票" width="80%" append-to-body>
<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>
<el-table-column prop="billNo" label="汇票单号" min-width="180" />
<el-table-column prop="issuerName" label="出票单位" min-width="160" />
<el-table-column prop="receiverName" label="收票单位" min-width="160" />
<el-table-column prop="faceAmount" label="票面金额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.faceAmount) }}</template>
</el-table-column>
<el-table-column prop="availableBalance" label="可用余额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.availableBalance) }}</template>
</el-table-column>
<el-table-column prop="maturityDate" label="到期日期" min-width="120" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="selectBill(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="payment-form-page__reference-pagination">
<el-pagination
v-model:current-page="billPage.current"
v-model:page-size="billPage.size"
:total="billPage.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadBillPage"
@size-change="handleBillSizeChange"
/>
</div>
</el-dialog>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
>
<pdf-preview
v-if="
attachmentDocumentPreviewVisible &&
attachmentPreviewFile.url &&
attachmentPreviewFile.isPdf
"
:source="attachmentPreviewFile.url"
@error="handleAttachmentPreviewError"
/>
<open-file-viewer
v-else-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
:file="attachmentPreviewFile.url"
:file-name="attachmentPreviewFile.name"
:mime-type="attachmentPreviewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="attachmentViewerToolbar"
:plugins="attachmentViewerPlugins"
@unsupported="handleAttachmentPreviewUnsupported"
@error="handleAttachmentPreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="attachmentImagePreviewVisible"
:url-list="attachmentImagePreviewUrls"
:initial-index="attachmentImagePreviewIndex"
@close="attachmentImagePreviewVisible = false"
/>
</basic-container>
</template>
<script>
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';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import { getMkPublicDetail } from '@/api/mk-process';
import * as billLedgerApi from '@/api/payment/billLedger';
import { getDictionary } from '@/api/system/dictbiz';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getDetail as getContractDetail } from '@/api/business/contract-manage';
import { getRisk as getCustomerRisk } from '@/api/business/hcs';
import * as formalApi from '@/api/settlement/formalSettlement';
import * as preApi from '@/api/settlement/preSettlement';
import {
getDetail as getCustomerArchiveDetail,
getList as getCustomerArchiveList,
} from '@/api/vehicle/customer-archive';
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl } from '@/utils/util';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import {
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import PdfPreview from '@/components/pdf-preview/main.vue';
const attachmentViewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
const paymentApplicationListRoute = '/payment/payment-application';
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
const ATTACHMENT_MATERIALS = {
weighingSlip: { key: 'weighingSlip', label: '磅单', keywords: ['磅单'] },
entrustOrder: { key: 'entrustOrder', label: '委托单', keywords: ['委托单'] },
settlement: { key: 'settlement', label: '结算单', keywords: ['结算单'] },
contract: {
key: 'contract',
label: '合同签章文件',
keywords: ['合同签章文件', '合同签章', '签章', '合同'],
},
specialApproval: {
key: 'specialApproval',
label: '特批附件',
keywords: ['特批附件', '特批'],
},
};
const MATERIAL_TYPE_MAP = {
weighingSlip: 'weighing_slip',
entrustOrder: 'entrust_order',
settlement: 'settlement',
contract: 'contract',
specialApproval: 'special_approval',
};
const emptyForm = () => ({
id: null,
paymentNo: '',
paymentType: '',
settlementId: null,
settlementIds: [],
preSettlementId: null,
preSettlementIds: [],
projectId: null,
projectName: '',
deptId: null,
deptName: '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
paymentRatio: null,
appliedAmount: 0,
paymentMethod: '',
billLedgerId: null,
billNo: '',
receiptAccountId: null,
receiptAccountName: '',
bankName: '',
bankAccount: '',
applicantName: '',
applyDate: '',
remark: '',
attachments: [],
invoices: [],
});
export default {
name: 'PaymentApplicationForm',
components: {
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
data() {
return {
form: emptyForm(),
amountSyncing: false,
transferPayload: null,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false,
referenceRows: [],
paymentRecords: [],
referenceVisible: false,
referenceTab: 'formal',
referenceQuery: {
settlementNo: '',
projectName: '',
contractName: '',
},
formalRows: [],
formalSelection: [],
preRows: [],
preSelection: [],
formalPage: {
current: 1,
size: 10,
total: 0,
loading: false,
},
prePage: {
current: 1,
size: 10,
total: 0,
loading: false,
},
projectOptions: [],
projectLoading: false,
contractOptions: [],
contractLoading: false,
contractRows: [],
billOptions: [],
billLoading: false,
billDialogVisible: false,
settlementPayerName: '',
settlementPayeeName: '',
billQuery: {
keyword: '',
},
billRows: [],
billPage: {
current: 1,
size: 10,
total: 0,
loading: false,
},
receiptAccountOptions: [],
receiptAccountLoading: false,
contractPartyBName: '',
firstContractPayment: false,
contractSignatureFileAvailable: false,
contractPaymentRatioLimit: null,
contractPaymentRatioExceeded: false,
/** 合同付款比例设置(按第1笔、第2笔…顺序) */
contractPaymentRatioRows: [],
payeeCustomerLevel: '',
// 收款方 C101-1 风险查询留档:risky=高风险/黑名单,tip=后端拼装提示文案
payeeRisky: false,
payeeRiskTip: '',
payeeRiskLevel: '',
payeeBlackStatus: '',
attachmentRuleLoading: false,
// 最近一次从结算单同步过来的附件 URL,切换结算单时用于整批替换
linkedSettlementAttachmentUrls: [],
attachmentImagePreviewVisible: false,
attachmentImagePreviewUrls: [],
attachmentImagePreviewIndex: 0,
attachmentDocumentPreviewVisible: false,
attachmentPreviewFile: {},
attachmentViewerPlugins,
attachmentViewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
paymentTypeOptions: api.paymentTypeOptions,
paymentMethodOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'gif',
'webp',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
'rar',
],
rules: {
paymentType: [{ required: true, message: '请选择付款类型' }],
referenceId: [
{ required: true, validator: this.validateReference, trigger: 'change' },
],
paymentMethod: [{ required: true, message: '请选择付款方式' }],
receiptAccountId: [{ required: true, message: '请选择收款方', trigger: 'change' }],
billLedgerId: [{ validator: this.validateBillLedger, trigger: 'change' }],
settlementAmount: [{ validator: this.validateSettlementAmount, trigger: 'change' }],
appliedAmount: [
{ required: true, validator: this.validateAppliedAmount, trigger: ['change', 'blur'] },
],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view' || this.isPublicViewPage;
},
isPublicViewPage() {
return this.$route.path === '/payment/payment-application/public-view';
},
canSave() {
const code = this.recordId ? 'payment_application_edit' : 'payment_application_add';
return this.hasPermission(code);
},
paymentTypeLocked() {
if (this.readonly || this.recordId) return false;
const queryType = String(this.$route.query.paymentType || '');
if (['progress_advance', 'settlement_payment'].includes(queryType)) return true;
if (this.transferPayload?.sourcePreSettlements?.length) return true;
if (this.transferPayload?.sourceFormalSettlements?.length) return true;
return false;
},
isSettlementRelatedType() {
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
},
// 项目预付、进度预付不展示发票信息,仅尾款付款展示
showInvoiceSection() {
return this.form.paymentType === 'settlement_payment';
},
canSelectPreSettlement() {
// 进度预付仅预结算;尾款付款可正式/预结算
return ['progress_advance', 'settlement_payment'].includes(this.form.paymentType);
},
canSelectFormalSettlement() {
return this.form.paymentType === 'settlement_payment';
},
referenceDialogTitle() {
if (this.form.paymentType === 'project_advance') return '选择项目合同';
if (this.canSelectPreSettlement && this.canSelectFormalSettlement) return '选择结算单';
if (this.canSelectPreSettlement) return '选择预付结算单';
if (this.canSelectFormalSettlement) return '选择付款结算单';
return '选择结算单';
},
billPayment() {
return this.isBillPaymentMethod();
},
paymentAmountBase() {
if (this.form.paymentType === 'project_advance') return 0;
const amount = this.form.settlementAmount;
return Number(amount || 0);
},
referenceSelection() {
return (this.referenceTab === 'formal' ? this.formalSelection : this.preSelection) || [];
},
referenceLabel() {
if (this.referenceRows.length) {
return this.referenceRows
.map(item => item.settlementNo)
.filter(Boolean)
.join('、');
}
return this.form.settlementNo || this.form.preSettlementNo || '';
},
referenceSettlementLinks() {
if (this.referenceRows.length) {
return this.referenceRows.filter(item => item.settlementNo);
}
const label = this.referenceLabel;
if (!label) return [];
return String(label)
.split(/[、,,]/)
.map(item => item.trim())
.filter(Boolean)
.map(settlementNo => ({
settlementNo,
id: this.form.preSettlementId
? this.form.preSettlementId || this.form.preSettlementIds?.[0]
: this.form.settlementId || this.form.settlementIds?.[0],
referenceType: this.form.preSettlementId ? 'pre' : 'formal',
}));
},
referenceSettlementNos() {
return this.referenceSettlementLinks.map(item => item.settlementNo).filter(Boolean);
},
referenceSettlementCount() {
return this.referenceSettlementNos.length;
},
// 结算单号展示:单张显示单号;多张仅提示数量,明细在下方「结算信息」表,悬停可看全部单号
referenceDisplayText() {
if (this.referenceSettlementCount > 1) return `已选 ${this.referenceSettlementCount} 张结算单`;
return this.referenceLabel;
},
settlementRows() {
const source = this.referenceRows.length
? this.referenceRows
: this.referenceLabel
? [
{
settlementNo: this.referenceLabel,
settlementAmount: this.form.settlementAmount,
payableAmount: this.form.payableAmount,
paymentRatio: null,
appliedAmount: null,
paymentSequence: 1,
},
]
: [];
// 结算行比例/金额只取行内数据,不回落到付款基本信息
return source.map(item => {
const paymentRatio = this.nullableNumber(item.paymentRatio);
const appliedAmount = this.nullableNumber(item.appliedAmount);
return {
...item,
paymentRatio,
appliedAmount,
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
};
});
},
highRiskPayee() {
// 收款方 C101-1 高风险/黑名单,或客商等级 D/E/F(本地评级口径),命中须上传特批附件
if (this.payeeRisky) return true;
const level = String(this.payeeCustomerLevel || '')
.trim()
.toUpperCase();
return ['D', 'D级', 'E', 'E级', 'F', 'F级'].includes(level) || level.includes('高风险');
},
// 高风险/黑名单收款方提示(C101-1,同合同新增页风险客商提示口径;强制拦截在后端提交校验与金蝶推送门禁)
payeeRiskText() {
if (!this.payeeRisky) return '';
const levelMap = { 0: '低风险', 1: '中风险', 2: '高风险' };
const level = levelMap[this.payeeRiskLevel] || '未知';
const blacklist = String(this.payeeBlackStatus || '').trim().toLowerCase() === 'yes' ? '是' : '否';
const tip = this.payeeRiskTip || `收款方[${this.form.payeeName || ''}]为高风险/黑名单客商`;
return `风险等级:${level}|黑名单:${blacklist}。${tip},按集团规定不可直接付款,提交前须上传「特批附件」类型附件,审批通过后将自动发起付款特批`;
},
requiredAttachmentMaterials() {
if (this.form.paymentType === 'project_advance') {
return [ATTACHMENT_MATERIALS.contract];
}
const materials = [ATTACHMENT_MATERIALS.weighingSlip, ATTACHMENT_MATERIALS.settlement];
if (this.firstContractPayment) materials.push(ATTACHMENT_MATERIALS.contract);
if (this.highRiskPayee) materials.push(ATTACHMENT_MATERIALS.specialApproval);
return materials;
},
missingAttachmentMaterials() {
return this.requiredAttachmentMaterials.filter(item => !this.hasAttachmentMaterial(item));
},
downloadableAttachments() {
return (this.form.attachments || []).filter(item => this.attachmentFileUrl(item));
},
},
watch: {
'$route.query.transferToken': async function (token, oldToken) {
if (!this.recordId && token && token !== oldToken) {
this.transferPayload = readSettlementTransfer(token);
await this.initialize();
}
},
'form.paymentRatio'(value) {
this.checkContractPaymentRatio(value);
// 上方比例仅用于反算申请付款金额,不联动结算信息列表
if (this.amountSyncing || !this.paymentAmountBase) return;
this.amountSyncing = true;
this.form.appliedAmount = Number(
((this.paymentAmountBase * Number(value || 0)) / 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
'form.appliedAmount'(value) {
// 上方申请付款金额反算比例,不联动结算信息列表
if (this.amountSyncing || !this.paymentAmountBase) return;
this.amountSyncing = true;
this.form.paymentRatio = Number(
((Number(value || 0) / this.paymentAmountBase) * 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
'form.settlementAmount'() {
if (
this.amountSyncing ||
this.form.paymentType === 'project_advance' ||
!this.paymentAmountBase ||
this.nullableNumber(this.form.paymentRatio) === null
) {
return;
}
// 结算金额变化时只按上方比例重算申请付款金额,不改结算信息明细
this.amountSyncing = true;
this.form.appliedAmount = Number(
((this.paymentAmountBase * Number(this.form.paymentRatio || 0)) / 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
},
created() {
this.transferPayload = readSettlementTransfer(this.$route.query.transferToken);
this.initialize();
},
methods: {
formatContractOptionLabel,
hasPermission(code) {
return this.permission?.[code] !== false;
},
validateReference(rule, value, callback) {
if (!this.isSettlementRelatedType) {
callback();
return;
}
if (
this.form.paymentType === 'progress_advance' &&
!this.form.preSettlementId &&
!this.form.preSettlementIds?.length
) {
callback(new Error('请选择预付结算单'));
return;
}
if (
this.form.paymentType === 'settlement_payment' &&
!this.form.settlementId &&
!this.form.settlementIds?.length &&
!this.form.preSettlementId &&
!this.form.preSettlementIds?.length
) {
callback(new Error('请输入'));
return;
}
callback();
},
refreshReferenceValidation() {
this.$nextTick(() => this.$refs.formRef?.validateField('referenceId').catch(() => {}));
},
validateSettlementAmount(rule, value, callback) {
if (this.form.paymentType === 'project_advance') {
callback();
return;
}
const amount = Number(value);
if (!Number.isFinite(amount) || amount < 0) {
callback(new Error('结算金额不能小于0'));
return;
}
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 (!Number.isFinite(amount)) {
callback(new Error('请填写申请付款金额'));
return;
}
if (amount <= 0) {
callback(new Error('申请付款金额必须大于0,不能为0元'));
return;
}
if (
this.form.paymentType !== 'project_advance' &&
payableAmount > 0 &&
amount > payableAmount
) {
callback(new Error('申请付款金额不能超过可付款金额'));
return;
}
callback();
},
validateBillLedger(rule, value, callback) {
if (!this.billPayment) {
callback();
return;
}
if (!value) {
callback(new Error('请选择汇票单号'));
return;
}
const selected = this.billOptions.find(item => String(item.id) === String(value));
if (!selected) {
callback();
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;
}
callback();
},
validateInvoices() {
if (!this.showInvoiceSection) return;
for (const invoice of this.form.invoices || []) {
if (String(invoice.settlementNo || '').length > 100) {
throw new Error('结算单号不能超过100个字符');
}
if (String(invoice.invoiceNo || '').length > 100) {
throw new Error('发票号不能超过100个字符');
}
if (String(invoice.invoiceType || '').length > 30) {
throw new Error('发票类型不能超过30个字符');
}
const invoiceAmount = Number(invoice.invoiceAmount || 0);
const matchedAmount = Number(invoice.matchedAmount || 0);
if (invoiceAmount < 0 || matchedAmount < 0) {
throw new Error('发票金额和匹配金额不能小于0');
}
if (matchedAmount > invoiceAmount) {
throw new Error('单张发票匹配金额不能超过发票金额');
}
}
},
validatePaymentRecords() {
if (this.form.paymentType === 'project_advance') return;
const appliedAmount = Number(this.form.appliedAmount || 0);
const paidAmount = (this.paymentRecords || []).reduce((total, record) => {
const amount = Number(record.paidAmount || 0);
if (!Number.isFinite(amount) || amount < 0) {
throw new Error('付款记录金额不能小于0');
}
if (String(record.paymentNo || '').length > 100) {
throw new Error('付款单号不能超过100个字符');
}
if (String(record.kingdeeBillNo || '').length > 100) {
throw new Error('付款凭证不能超过100个字符');
}
return total + amount;
}, 0);
if (paidAmount > appliedAmount) {
throw new Error('付款记录金额合计不能超过申请付款金额');
}
},
formatLocalDate(value) {
if (!value) return null;
const date = this.$dayjs(value);
return date.isValid() ? date.format('YYYY-MM-DD') : null;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async loadPublicDetail() {
if (!this.recordId) return;
const data = this.unwrapData(
await getMkPublicDetail('payment-application', this.recordId)
);
this.form = {
...emptyForm(),
...data,
preSettlementIds: data.preSettlementIds?.length
? data.preSettlementIds
: data.preSettlementId
? [data.preSettlementId]
: [],
attachments: this.parse(data.attachmentsJson),
invoices: data.invoices || [],
};
if (this.form.paymentMethod) {
this.paymentMethodOptions = [
{
label: this.form.paymentMethodName || this.form.paymentMethod,
value: this.form.paymentMethod,
},
];
}
if (this.form.projectId) {
this.projectOptions = [
{
id: this.form.projectId,
projectName: this.form.projectName || this.form.projectCode,
},
];
}
if (this.form.contractId) {
this.contractOptions = [
{
id: this.form.contractId,
contractName: this.form.contractName || this.form.contractNo,
},
];
}
const referenceRow = this.createReferenceRow(data);
this.referenceRows = this.restoreReferenceRows(data, referenceRow);
if (this.referenceRows.length > 1) this.syncHeaderPaymentFromRows();
this.paymentRecords = data.paymentRecords || [];
},
async initialize() {
if (this.isPublicViewPage) {
await this.loadPublicDetail();
return;
}
await this.loadPaymentMethodOptions();
if (this.recordId) {
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
preSettlementIds: data.preSettlementIds?.length
? data.preSettlementIds
: data.preSettlementId
? [data.preSettlementId]
: [],
attachments: this.parse(data.attachmentsJson),
invoices: data.invoices || [],
};
const referenceRow = this.createReferenceRow(data);
this.referenceRows = this.restoreReferenceRows(data, referenceRow);
this.clearFormalReferenceWhenPreOnly();
this.paymentRecords = data.paymentRecords || [];
if (this.form.paymentType === 'project_advance') {
await this.loadProjectOptions();
await this.loadContractOptions(this.form.projectId);
}
await this.fillBillTypeFromContract(this.form.contractId);
await this.refreshSettlementPaymentSequences();
if (this.referenceRows.length > 1) this.syncHeaderPaymentFromRows();
await this.loadAttachmentRuleData(await this.loadCurrentSettlementDetails());
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
} else {
this.form = emptyForm();
this.form.paymentMethod = this.paymentMethodOptions[0]?.value || '';
this.form.paymentType = this.initialAddPaymentType();
this.referenceRows = [];
this.form.applyDate = this.$dayjs().format('YYYY-MM-DD');
this.form.applicantName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
if (this.form.paymentType === 'project_advance') await this.loadProjectOptions();
if (this.transferPayload?.sourceFormalSettlements?.length) {
await this.loadTransferredFormalSettlements();
} else {
await this.loadTransferredPreSettlements();
}
}
},
initialAddPaymentType() {
const paymentType = String(this.$route.query.paymentType || '');
return api.paymentTypeOptions.some(item => item.value === paymentType) ? paymentType : '';
},
async fillBillTypeFromContract(contractId, autoSelectDefault = false) {
if (!contractId) {
this.form.billType = '';
this.contractPartyBName = '';
this.receiptAccountOptions = [];
this.resetContractPaymentRatioLimit();
return;
}
try {
const data = this.unwrapData(await getContractDetail(contractId));
this.form.billType = data?.settlementMode || '';
// 付款方取合同甲方名称
const partyA = data?.partyA || data?.payerName || '';
if (partyA) {
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() {
try {
const data = this.unwrapData(await getDictionary({ code: 'pay_method' }));
const records = Array.isArray(data) ? data : data?.records || [];
this.paymentMethodOptions = records
.map(item => ({
...item,
label: item.dictValue || item.label || item.name || item.dictKey || item.value,
value: item.dictKey || item.value || item.dictValue || item.name,
}))
.filter(item => item.label && item.value);
} catch (error) {
this.paymentMethodOptions = [];
this.$message.warning('付款方式字典加载失败,请稍后重试');
}
},
isBillPaymentMethod(value = this.form.paymentMethod) {
const selected = this.paymentMethodOptions.find(
item => String(item.value) === String(value)
);
const searchableText = [
selected?.dictKey,
selected?.dictValue,
selected?.label,
selected?.name,
selected?.value,
]
.filter(Boolean)
.join(' ');
return searchableText.includes('汇票');
},
getTransferredPreSettlementIds() {
const payloadIds = (this.transferPayload?.sourcePreSettlements || []).map(
item => item.preSettlementId || item.id
);
const queryIds = String(this.$route.query.preSettlementIds || '')
.split(',')
.map(item => item.trim())
.filter(Boolean);
return [...new Set([...payloadIds, ...queryIds].map(String))].filter(id =>
this.isValidReferenceId(id)
);
},
isValidReferenceId(id) {
const value = String(id ?? '').trim();
return value !== '' && value !== '-1' && value !== '0';
},
async loadTransferredPreSettlements() {
const fallbackRows = this.transferPayload?.sourcePreSettlements || [];
const ids = this.getTransferredPreSettlementIds();
if (!ids.length) {
await this.applyTransferredPreSettlements(fallbackRows);
await this.loadAttachmentRuleData(fallbackRows);
return;
}
try {
const responses = await Promise.all(ids.map(id => preApi.getDetail(id)));
const rows = responses.map(response => this.unwrapData(response)).filter(item => item?.id);
const sourceRows = rows.length ? rows : fallbackRows;
await this.applyTransferredPreSettlements(sourceRows);
await this.loadAttachmentRuleData(sourceRows);
} catch (error) {
if (fallbackRows.length) {
await this.applyTransferredPreSettlements(fallbackRows);
await this.loadAttachmentRuleData(fallbackRows);
return;
}
this.$message.error('预结算信息加载失败,请返回后重新发起预付申请');
throw error;
}
},
async loadTransferredFormalSettlements() {
const fallbackRows = this.transferPayload?.sourceFormalSettlements || [];
const rows = await Promise.all(
fallbackRows.map(async row => {
if (!this.isValidReferenceId(row.id)) return row;
try {
return { ...row, ...this.unwrapData(await formalApi.getDetail(row.id)) };
} catch (error) {
return row;
}
})
);
await this.applyTransferredFormalSettlements(
rows.filter(item => this.isValidReferenceId(item?.id))
);
await this.loadAttachmentRuleData(rows);
},
async applyTransferredFormalSettlements(rows = []) {
if (!rows.length) return;
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
if (contractIds.size !== 1 || contractIds.has('')) {
this.$message.warning('所选付款结算单合同信息不一致,请返回后重新选择');
return;
}
const first = rows[0];
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 || '',
};
});
this.seedMultipleRowPayments(referenceRows);
const settlementAmount = referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0),
0
);
const payableAmount = referenceRows.reduce(
(total, item) => total + Number(item.payableAmount || 0),
0
);
const settlementIds = rows.map(item => item.settlementId || item.id).filter(Boolean);
this.amountSyncing = true;
this.referenceRows = referenceRows;
Object.assign(this.form, {
paymentType: 'settlement_payment',
settlementId: settlementIds[0] || null,
settlementIds,
preSettlementId: null,
preSettlementIds: [],
settlementNo: referenceRows
.map(item => item.settlementNo)
.filter(Boolean)
.join('、'),
preSettlementNo: '',
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
contractId: first.contractId,
contractNo: first.contractNo,
contractName: first.contractName,
payerName: first.payerName,
payeeName: '',
settlementAmount: Number(settlementAmount.toFixed(2)),
payableAmount: Number(payableAmount.toFixed(2)),
billType: '',
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
invoices: rows.flatMap(item =>
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
),
});
this.rememberSettlementParties(first.payerName, first.payeeName);
this.paymentRecords = rows.flatMap(item =>
(item.paymentRecords || item.paymentApplications || []).map(record => ({
...record,
paidAmount: Number(record.paidAmount || 0),
paidDate: record.paidDate || record.paymentDate || record.createTime || '',
paymentNo: record.paymentNo || '',
voucherJson: record.voucherJson || '',
kingdeeBillNo: record.kingdeeBillNo || '',
}))
);
this.clearReceiptAccount();
await this.fillBillTypeFromContract(first.contractId, true);
this.$nextTick(() => {
this.amountSyncing = false;
this.$refs.formRef?.clearValidate();
});
},
clearFormalReferenceWhenPreOnly() {
const progressAdvance = this.form.paymentType === 'progress_advance';
const onlyPre =
progressAdvance ||
(this.referenceRows.length > 0 &&
this.referenceRows.every(item => item.referenceType === 'pre'));
if (!onlyPre) return;
if (!this.form.preSettlementId) {
const preId =
this.referenceRows.find(item => item.referenceType === 'pre')?.id ||
(progressAdvance ? this.form.settlementId : null);
if (preId) {
this.form.preSettlementId = preId;
this.form.preSettlementIds = [preId];
}
}
this.form.settlementId = null;
this.form.settlementIds = [];
this.form.settlementNo = '';
},
restoreReferenceRows(data, fallbackRow) {
const relations = Array.isArray(data?.settlements) ? data.settlements : [];
const rows = relations
.map(item => {
const isPre = Boolean(item.preSettlementId || item.preSettlementNo);
const settlementNo = item.preSettlementNo || item.formalSettlementNo || '';
if (!settlementNo) return null;
return {
id: isPre ? item.preSettlementId || item.formalSettlementId : item.formalSettlementId,
settlementNo,
referenceType: isPre ? 'pre' : 'formal',
settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)),
payableAmount: Number(Number(item.payableAmount ?? item.settlementAmount ?? 0).toFixed(2)),
appliedAmount: this.nullableNumber(item.appliedAmount),
paymentRatio: Number(item.settlementAmount || 0)
? Number(
(
(Number(item.appliedAmount || 0) / Number(item.settlementAmount || 0)) *
100
).toFixed(2)
)
: null,
};
})
.filter(Boolean);
if (rows.length) {
this.form.preSettlementIds = rows.filter(item => item.referenceType === 'pre').map(item => item.id);
this.form.settlementIds = rows.filter(item => item.referenceType === 'formal').map(item => item.id);
if (this.form.preSettlementIds.length) this.form.preSettlementId = this.form.preSettlementIds[0];
if (this.form.settlementIds.length) this.form.settlementId = this.form.settlementIds[0];
return rows;
}
return fallbackRow?.settlementNo ? [fallbackRow] : [];
},
createReferenceRow(row) {
const settlementAmount = Number(row.settlementAmount || 0);
const isPre = Boolean(
row.preSettlementNo ||
row.preSettlementId ||
row.referenceType === 'pre' ||
row.paymentType === 'progress_advance'
);
const paymentSequence = Number(row.paymentSequence);
// 可付款金额必须来自 referenceAmount 接口(已扣减占用中的付款申请);缺失时置 0,避免误用未扣减结算金额
const payableAmount =
row.payableAmount === undefined || row.payableAmount === null
? 0
: Number(row.payableAmount || 0);
return {
id:
row.preSettlementId ||
(isPre ? row.settlementId : null) ||
row.settlementId ||
(row.paymentNo ? null : row.id),
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal',
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
payableAmount: Number(Math.max(0, payableAmount).toFixed(2)),
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
paymentRatio: null,
appliedAmount: null,
};
},
nullableNumber(value) {
if (value === undefined || value === null || value === '') return null;
const amount = Number(value);
return Number.isFinite(amount) ? amount : null;
},
/** 付款基本信息内部:已填写比例时,按结算金额重算申请付款金额;不读取结算行付款金额。 */
headerAppliedAmountByRatio(settlementAmount) {
if (this.nullableNumber(this.form.paymentRatio) === null) return this.form.appliedAmount;
return Number(((Number(settlementAmount || 0) * Number(this.form.paymentRatio)) / 100).toFixed(2));
},
/**
* 按合同付款比例配置,判断当前付款次序是否超上限。
* @param {Object} row 结算信息行(含 paymentSequence)
*/
buildSettlementRatioHint(row = {}) {
const sequence = Number(row.paymentSequence || 1);
const rawRatio = row.paymentRatio;
const paymentRatio = Number(rawRatio);
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 =
rawRatio !== null &&
rawRatio !== undefined &&
String(rawRatio).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,
invoiceNo: invoice.invoiceNo || '',
invoiceDate: invoice.invoiceDate || '',
invoiceType: invoice.invoiceType || '',
taxRate: Number(invoice.taxRate || 0),
invoiceAmount: Number(invoice.invoiceAmount || 0),
availableInvoiceAmount: Number(invoice.availableInvoiceAmount || 0),
matchedAmount: Number(invoice.matchedAmount || 0),
attachmentJson: invoice.attachmentJson || '',
}));
},
syncPaymentRecords() {
const appliedCents = Math.max(0, Math.round(Number(this.form.appliedAmount || 0) * 100));
if (!appliedCents) {
this.$message.warning('请先填写申请付款金额');
return;
}
const maxCount = Math.min(5, appliedCents);
const minCount = Math.min(2, maxCount);
const recordCount = Math.floor(Math.random() * (maxCount - minCount + 1)) + minCount;
const minimumTotal = recordCount;
const randomTotal = Math.round(appliedCents * (0.5 + Math.random() * 0.5));
let remainingCents = Math.max(minimumTotal, Math.min(appliedCents, randomTotal));
const timestamp = this.$dayjs().format('YYYYMMDDHHmmss');
this.paymentRecords = Array.from({ length: recordCount }, (_unusedItem, index) => {
const remainingCount = recordCount - index - 1;
const maxCurrentCents = remainingCents - remainingCount;
const paidCents =
remainingCount === 0 ? remainingCents : Math.floor(Math.random() * maxCurrentCents) + 1;
remainingCents -= paidCents;
const sequence = String(index + 1).padStart(2, '0');
const paidDate = this.$dayjs()
.subtract(Math.floor(Math.random() * 30), 'day')
.format('YYYY-MM-DD');
return {
paidAmount: Number((paidCents / 100).toFixed(2)),
paidDate,
paymentNo: `MOCK-PAY-${timestamp}-${sequence}`,
voucherJson: '',
kingdeeBillNo: `MOCK-KD-${timestamp}-${sequence}`,
};
});
this.$message.success(`同步成功,已生成${recordCount}条付款记录`);
},
async applyTransferredPreSettlements(rows = []) {
if (!rows.length) return;
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
if (contractIds.size > 1 || contractIds.has('')) {
this.$message.warning('所选预付结算单合同信息不一致,请返回后重新选择');
return;
}
const first = rows[0];
this.form.invoices = [];
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
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;
}
this.seedMultipleRowPayments(this.referenceRows);
const settlementAmount = this.referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0),
0
);
const payableAmount = this.referenceRows.reduce(
(total, item) => total + Number(item.payableAmount || 0),
0
);
// 尾款付款场景下选择预付结算单时保留尾款付款类型,不强制改为进度预付
this.amountSyncing = true;
Object.assign(this.form, {
paymentType,
settlementId: null,
settlementIds: [],
settlementNo: '',
preSettlementId: first.preSettlementId || first.id,
preSettlementIds: rows.map(item => item.preSettlementId || item.id),
preSettlementNo: this.referenceRows
.map(item => item.settlementNo)
.filter(Boolean)
.join('、'),
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
contractId: first.contractId,
contractNo: first.contractNo,
contractName: first.contractName,
payerName: first.payerName,
payeeName: '',
settlementAmount: Number(settlementAmount.toFixed(2)),
payableAmount: Number(payableAmount.toFixed(2)),
billType: '',
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
});
this.rememberSettlementParties(first.payerName, first.payeeName);
this.paymentRecords = [];
this.clearReceiptAccount();
await this.fillBillTypeFromContract(first.contractId, true);
this.$nextTick(() => {
this.amountSyncing = false;
this.$refs.formRef?.clearValidate();
});
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
invoiceAttachment(row = {}) {
const value = row.attachmentJson;
if (!value) return {};
if (typeof value === 'object') return Array.isArray(value) ? value[0] || {} : value;
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed[0] || {} : parsed || {};
} catch {
return {};
}
},
invoiceAttachmentUrl(row) {
return this.attachmentFileUrl(this.invoiceAttachment(row));
},
invoiceAttachmentName(row) {
return (
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
);
},
async viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持本地预览组件
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
}
return;
}
if (!this.attachmentFileUrl(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
}
this.previewAttachment(
{ ...attachment, name: this.invoiceAttachmentName(row) },
[attachment]
);
},
async downloadInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
}
return;
}
const url = this.attachmentFileUrl(attachment);
if (!url) {
this.$message.warning('当前发票暂无可下载附件');
return;
}
downloadFileByUrl(url, this.invoiceAttachmentName(row));
},
attachmentFileName(file) {
return String(file?.originalName || file?.name || file?.fileName || '').trim();
},
normalizedAttachmentFileName(file) {
let fileName = this.attachmentFileName(file) || this.attachmentFileUrl(file);
fileName = String(fileName || '').split(/[?#]/)[0];
try {
fileName = decodeURIComponent(fileName);
} catch {}
return fileName.toLocaleLowerCase();
},
attachmentFileUrl(file) {
return (
file?.url ||
file?.link ||
file?.fileUrl ||
file?.downloadUrl ||
file?.src ||
file?.domain ||
''
);
},
attachmentFileExtension(file = {}) {
const name = String(this.attachmentFileName(file) || this.attachmentFileUrl(file)).split(
'?'
)[0];
const index = name.lastIndexOf('.');
return index > -1 ? name.slice(index + 1).toLowerCase() : '';
},
attachmentMimeType(file = {}) {
return String(file.mimeType || file.contentType || file.type || '').toLowerCase();
},
isAttachmentImage(file) {
return (
this.attachmentMimeType(file).startsWith('image/') ||
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentFileExtension(file))
);
},
isAttachmentPdf(file) {
return (
this.attachmentMimeType(file).includes('application/pdf') ||
this.attachmentFileExtension(file) === 'pdf'
);
},
previewAttachment(file, files = this.form.attachments) {
const url = this.attachmentFileUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isAttachmentImage(file)) {
this.attachmentImagePreviewUrls = (files || [])
.filter(item => this.isAttachmentImage(item) && this.attachmentFileUrl(item))
.map(item => this.attachmentFileUrl(item));
this.attachmentImagePreviewIndex = Math.max(
this.attachmentImagePreviewUrls.indexOf(url),
0
);
this.attachmentImagePreviewVisible = true;
return;
}
const isPdf = this.isAttachmentPdf(file);
this.attachmentPreviewFile = {
name: this.attachmentFileName(file) || '附件',
url,
mimeType: isPdf ? 'application/pdf' : this.attachmentMimeType(file),
isPdf,
};
this.attachmentDocumentPreviewVisible = true;
},
handleAttachmentPreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handleAttachmentPreviewError() {
this.$message.error('附件预览失败');
},
attachmentMatchesMaterial(file, material) {
const fileName = this.normalizedAttachmentFileName(file);
return (
Boolean(this.attachmentFileUrl(file)) &&
(file.attachmentType === MATERIAL_TYPE_MAP[material.key] ||
material.keywords.some(keyword => fileName.includes(keyword.toLocaleLowerCase())))
);
},
matchAttachmentMaterialByFileName(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
const fileName = this.normalizedAttachmentFileName(file);
if (!fileName) return null;
const matched = (materials || [])
.flatMap(material =>
material.keywords.map(keyword => ({
material,
keyword: String(keyword).toLocaleLowerCase(),
}))
)
.filter(item => item.keyword)
.sort((a, b) => b.keyword.length - a.keyword.length)
.find(item => fileName.includes(item.keyword));
return matched?.material || null;
},
resolveAttachmentTypeByFileName(file) {
const material = this.matchAttachmentMaterialByFileName(file);
return material ? MATERIAL_TYPE_MAP[material.key] : 'other';
},
hasAttachmentMaterial(material) {
const hasUploadedAttachment = (this.form.attachments || []).some(file =>
this.attachmentMatchesMaterial(file, material)
);
if (
this.form.paymentType === 'project_advance' &&
material.key === ATTACHMENT_MATERIALS.contract.key
) {
return this.contractSignatureFileAvailable || hasUploadedAttachment;
}
return hasUploadedAttachment;
},
resolveAttachmentMaterial(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
if (!this.attachmentFileUrl(file)) return null;
const typeText = String(file.attachmentType || file.type || '')
.trim()
.toLocaleLowerCase();
const typeMatched = (materials || []).find(material => {
if (file.attachmentType === MATERIAL_TYPE_MAP[material.key]) return true;
if (!typeText) return false;
const label = String(material.label || '').toLocaleLowerCase();
return (
typeText === label ||
material.keywords.some(keyword => {
const normalizedKeyword = String(keyword).toLocaleLowerCase();
return normalizedKeyword && typeText.includes(normalizedKeyword);
})
);
});
return typeMatched || this.matchAttachmentMaterialByFileName(file, materials);
},
mergeAutomaticAttachments(
files,
materials,
sourceName,
defaultMaterial = null,
sourceKey = '',
keepUnmatched = false
) {
const existingUrls = new Set(
(this.form.attachments || []).map(file => this.attachmentFileUrl(file)).filter(Boolean)
);
const imported = (files || [])
.map(file => ({
file,
material: this.resolveAttachmentMaterial(file, materials) || defaultMaterial,
}))
.filter(item => {
const url = this.attachmentFileUrl(item.file);
return Boolean(url) && !existingUrls.has(url) && (item.material || keepUnmatched);
})
.map(({ file, material }) => {
const url = this.attachmentFileUrl(file);
existingUrls.add(url);
return {
...file,
originalName: this.attachmentFileName(file),
name: this.attachmentFileName(file),
url,
link: file.link || url,
attachmentType: material ? MATERIAL_TYPE_MAP[material.key] : 'other',
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 = Object.values(ATTACHMENT_MATERIALS);
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',
true
);
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.form.paymentType === 'settlement_payment') &&
uniquePreIds.length
) {
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' && 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 [];
},
async loadAttachmentRuleData(settlementRows = []) {
this.attachmentRuleLoading = true;
this.firstContractPayment = false;
this.contractSignatureFileAvailable = false;
this.resetContractPaymentRatioLimit();
try {
// 切换结算单时,附件区同步替换为当前结算单已上传附件
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') {
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,
'contract'
);
return;
}
const paymentResponse = await api.getList(1, 9999, {
contractId: this.form.contractId,
});
const payments = this.unwrapData(paymentResponse)?.records || [];
this.firstContractPayment = !payments.some(
item =>
String(item.contractId || '') === String(this.form.contractId) &&
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,
'contract'
);
}
} catch (error) {
this.$message.warning('附件清单来源材料加载失败,请核对后手工补充');
} finally {
this.attachmentRuleLoading = false;
}
},
resetContractPaymentRatioLimit() {
this.contractPaymentRatioLimit = null;
this.contractPaymentRatioExceeded = false;
this.contractPaymentRatioRows = [];
},
applyContractPaymentRatioLimit(contract = {}) {
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 =
ratioLimitValue === null ||
ratioLimitValue === undefined ||
String(ratioLimitValue).trim() === '' ||
!Number.isFinite(ratioLimit)
? null
: ratioLimit;
this.checkContractPaymentRatio(this.form.paymentRatio);
},
checkContractPaymentRatio(value) {
const paymentRatio = Number(value);
const hasPaymentRatio =
value !== null &&
value !== undefined &&
String(value).trim() !== '' &&
Number.isFinite(paymentRatio);
const exceeded =
this.form.paymentType === 'project_advance' &&
Boolean(this.form.contractId) &&
this.contractPaymentRatioLimit !== null &&
hasPaymentRatio &&
paymentRatio > this.contractPaymentRatioLimit;
if (exceeded && !this.contractPaymentRatioExceeded) {
this.$message.warning('已超合同配置比例');
}
this.contractPaymentRatioExceeded = exceeded;
},
handleTypeChange() {
// 切换类型时清空发票;预付类型本身也不再展示/提交发票
this.form.invoices = [];
this.paymentRecords = [];
Object.assign(this.form, {
settlementId: null,
settlementIds: [],
preSettlementId: null,
preSettlementIds: [],
settlementNo: '',
preSettlementNo: '',
projectId: null,
projectName: '',
deptId: null,
deptName: '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: '',
});
this.referenceRows = [];
this.contractOptions = [];
this.contractPartyBName = '';
this.receiptAccountOptions = [];
this.clearReceiptAccount();
this.loadAttachmentRuleData([]);
if (this.form.paymentType === 'project_advance') this.loadProjectOptions();
},
clearReceiptAccount() {
Object.assign(this.form, {
receiptAccountId: null,
receiptAccountName: '',
payeeName: '',
bankName: '',
bankAccount: '',
});
},
customerRecords(response) {
return this.unwrapData(response)?.records || [];
},
async loadReceiptAccountOptions(autoSelectDefault = true) {
const partyBName = String(this.contractPartyBName || '').trim();
const preserveCurrentValue = autoSelectDefault === false;
this.receiptAccountOptions = [];
this.payeeCustomerLevel = '';
this.payeeRisky = false;
this.payeeRiskTip = '';
this.payeeRiskLevel = '';
this.payeeBlackStatus = '';
if (!partyBName) {
if (!preserveCurrentValue) this.clearReceiptAccount();
return;
}
this.receiptAccountLoading = true;
try {
const [fullNameResponse, shortNameResponse] = await Promise.all([
getCustomerArchiveList(1, 20, {
fullName: partyBName,
approvalStatus: 'approved',
status: 1,
}),
getCustomerArchiveList(1, 20, {
shortName: partyBName,
approvalStatus: 'approved',
status: 1,
}),
]);
const customers = [
...this.customerRecords(fullNameResponse),
...this.customerRecords(shortNameResponse),
];
const customer = customers.find(
item => item.fullName === partyBName || item.shortName === partyBName
);
if (!customer?.id) {
if (!preserveCurrentValue) this.clearReceiptAccount();
return;
}
const detail = this.unwrapData(await getCustomerArchiveDetail(customer.id));
this.payeeCustomerLevel = detail.customerLevel || '';
// 收款方实时查 C101-1 客商风险(后端对内部组织客商默认返回无风险):高风险/黑名单在
// 基本信息"风险客商提示"条常驻展示并要求特批附件;查询失败不阻断填写,提交时后端强校验兜底
if (detail.customerCode) {
try {
const risk = this.unwrapData(await getCustomerRisk(detail.customerCode)) || {};
this.payeeRisky = Boolean(risk.risky);
this.payeeRiskTip = risk.riskTip || '';
this.payeeRiskLevel = risk.fdRiskLevel || '';
this.payeeBlackStatus = risk.fdBlackStatus || '';
} catch (error) {
// 忽略风险查询异常,提交时后端 C101-1 强校验兜底
}
}
this.receiptAccountOptions = (detail.receiptAccounts || [])
.filter(item => item.accountName || item.accountHolderName || item.bankAccount)
.map(item => ({
...item,
id: String(item.id),
}));
const selected = this.receiptAccountOptions.find(
item => String(item.id) === String(this.form.receiptAccountId || '')
);
if (selected) {
this.applyReceiptAccount(selected);
return;
}
if (preserveCurrentValue) {
// 编辑回填时若选项已加载但暂未匹配到,保留已有展示字段
return;
}
this.clearReceiptAccount();
if (autoSelectDefault && !this.readonly) {
const defaultAccount =
this.receiptAccountOptions.find(item => Number(item.isDefault) === 1) ||
(this.receiptAccountOptions.length === 1 ? this.receiptAccountOptions[0] : null);
if (defaultAccount) this.applyReceiptAccount(defaultAccount);
}
} catch (error) {
if (!preserveCurrentValue) this.clearReceiptAccount();
this.$message.warning('合同乙方客商收款信息加载失败,请稍后重试');
} finally {
this.receiptAccountLoading = false;
}
},
payeeReceiptLabel(item) {
return item.accountName || item.accountHolderName || '未命名收款方';
},
receiptAccountLabel(item) {
return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount]
.filter(Boolean)
.join('|');
},
applyReceiptAccount(account) {
Object.assign(this.form, {
receiptAccountId: account.id,
receiptAccountName: account.accountName || account.accountHolderName || '',
payeeName: account.accountName || account.accountHolderName || '',
bankName: account.bankName || '',
bankAccount: account.bankAccount || '',
});
this.$nextTick(() => this.$refs.formRef?.validateField('receiptAccountId').catch(() => {}));
},
handlePayeeReceiptChange(id) {
const account = this.receiptAccountOptions.find(item => String(item.id) === String(id || ''));
if (account) {
this.applyReceiptAccount(account);
return;
}
this.clearReceiptAccount();
},
handleReceiptAccountChange(id) {
this.handlePayeeReceiptChange(id);
},
async loadProjectOptions() {
if (this.projectLoading) return;
this.projectLoading = true;
try {
const response = await getProjectList(1, 9999, {});
this.projectOptions = this.customerRecords(response);
if (
this.form.projectId &&
this.form.projectName &&
!this.projectOptions.some(item => String(item.id) === String(this.form.projectId))
) {
this.projectOptions.unshift({
id: this.form.projectId,
projectName: this.form.projectName,
});
}
} catch (error) {
this.projectOptions = [];
this.$message.warning('项目列表加载失败,请稍后重试');
} finally {
this.projectLoading = false;
}
},
async loadContractOptions(projectId) {
if (!projectId) {
this.contractOptions = [];
return;
}
this.contractLoading = true;
try {
const response = await formalApi.getContractOptions('', projectId);
this.contractOptions = (this.unwrapData(response) || []).filter(
item =>
String(item.projectId || '') === String(projectId) &&
String(item.contractCategory || '').trim() === '承运商合同'
);
} catch (error) {
this.contractOptions = [];
this.$message.warning('项目合同加载失败,请稍后重试');
} finally {
this.contractLoading = false;
}
},
async handleProjectChange(projectId) {
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
this.form.projectName = project?.projectName || project?.projectShortName || '';
Object.assign(this.form, {
deptId:
String(project?.undertakeDeptId || '')
.split(/[,,]/)
.map(item => item.trim())
.find(Boolean) ||
project?.businessDeptId ||
null,
deptName:
String(project?.undertakeDeptName || '')
.split(/[,,、]/)
.map(item => item.trim())
.find(Boolean) ||
project?.businessDeptName ||
'',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: '',
});
this.contractOptions = [];
this.clearReceiptAccount();
this.form.invoices = [];
this.paymentRecords = [];
await this.loadContractOptions(projectId);
await this.loadAttachmentRuleData([]);
},
async handleAdvanceContractChange(contractId) {
const contract = this.contractOptions.find(
item => String(item.id) === String(contractId)
);
if (!contract) {
Object.assign(this.form, {
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
});
this.contractPartyBName = '';
this.receiptAccountOptions = [];
this.clearReceiptAccount();
return;
}
Object.assign(this.form, {
contractNo: contract.contractNo || '',
contractName: contract.contractName || '',
payerName: contract.payerName || contract.partyA || '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
});
this.contractPartyBName = String(contract.partyB || contract.payeeName || '').trim();
this.clearReceiptAccount();
await Promise.all([
this.fillBillTypeFromContract(contract.id, true),
this.loadAttachmentRuleData([]),
]);
if (this.billPayment) this.loadBillOptions();
},
async handlePaymentMethodChange() {
if (!this.billPayment) {
this.form.billLedgerId = null;
this.form.billNo = '';
this.billOptions = [];
this.billRows = [];
this.billDialogVisible = false;
return;
}
await this.loadBillOptions();
},
async loadBillOptions(keyword = '', selectedId = this.form.billLedgerId) {
if (!this.billPayment) return;
this.billLoading = true;
try {
this.billOptions =
this.unwrapData(
await billLedgerApi.getAvailableOptions(
keyword,
undefined,
selectedId,
this.settlementPayerName,
this.settlementPayeeName
)
) || [];
} finally {
this.billLoading = false;
}
},
async openBillDialog() {
if (this.readonly || !this.billPayment) return;
this.billDialogVisible = true;
this.billQuery.keyword = '';
this.billPage.current = 1;
await this.loadBillPage();
},
async loadBillPage() {
if (!this.billPayment) return;
this.billPage.loading = true;
try {
const response = await billLedgerApi.getAvailablePage(
this.billPage.current,
this.billPage.size,
String(this.billQuery.keyword || '').trim() || undefined,
undefined,
this.form.billLedgerId,
this.settlementPayerName,
this.settlementPayeeName
);
const data = this.unwrapData(response) || {};
this.billRows = data.records || [];
this.billPage.total = Number(data.total || 0);
} finally {
this.billPage.loading = false;
}
},
handleBillSearch() {
this.billPage.current = 1;
this.loadBillPage();
},
resetBillSearch() {
this.billQuery.keyword = '';
this.handleBillSearch();
},
handleBillSizeChange() {
this.billPage.current = 1;
this.loadBillPage();
},
rememberSettlementParties(payerName, payeeName) {
this.settlementPayerName = String(payerName || '').trim();
this.settlementPayeeName = String(payeeName || '').trim();
},
billMatchesSettlementParties(row) {
const issuer = String(row?.issuerName || '').trim();
const receiver = String(row?.receiverName || '').trim();
const payer = this.settlementPayerName;
const payee = this.settlementPayeeName;
if (!issuer || !receiver || !payer || !payee) return false;
return (
(issuer === payer && receiver === payee) || (issuer === payee && receiver === payer)
);
},
selectBill(row) {
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];
this.billDialogVisible = false;
this.$nextTick(() => this.$refs.formRef?.validateField('billLedgerId').catch(() => {}));
},
referenceParams(type) {
const { settlementNo, projectName, contractName } = this.referenceQuery;
return {
settlementType: 'payable',
approvalStatus: 'approved',
[type === 'formal' ? 'formalSettlementNo' : 'preSettlementNo']:
String(settlementNo || '').trim() || undefined,
projectName: String(projectName || '').trim() || undefined,
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 {
const response = await formalApi.getList(
this.formalPage.current,
this.formalPage.size,
this.referenceParams('formal')
);
const data = this.unwrapData(response) || {};
// 已调整的结算单为调整重推后的归档原单,金额已作废,不可再发起付款
const records = (data.records || []).filter(row => row.adjustStatus !== 'adjusted');
this.formalRows = await this.fillReferencePayableAmounts(records, 'settlement_payment');
this.formalPage.total = Number(data.total || 0);
} finally {
this.formalPage.loading = false;
}
},
async loadPreReferences() {
this.prePage.loading = true;
try {
const response = await preApi.getList(
this.prePage.current,
this.prePage.size,
this.referenceParams('pre')
);
const data = this.unwrapData(response) || {};
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;
}
},
loadSettlementReferences() {
const loaders = [];
if (this.canSelectFormalSettlement) loaders.push(this.loadFormalReferences());
if (this.canSelectPreSettlement) loaders.push(this.loadPreReferences());
if (!loaders.length) {
return Promise.all([this.loadFormalReferences(), this.loadPreReferences()]);
}
return Promise.all(loaders);
},
handleReferenceSearch() {
this.formalPage.current = 1;
this.prePage.current = 1;
this.loadSettlementReferences();
},
resetReferenceSearch() {
Object.assign(this.referenceQuery, {
settlementNo: '',
projectName: '',
contractName: '',
});
this.handleReferenceSearch();
},
handleReferenceTabChange(name) {
if (name === 'formal') {
this.loadFormalReferences();
return;
}
this.loadPreReferences();
},
handleFormalSizeChange() {
this.formalPage.current = 1;
this.loadFormalReferences();
},
handlePreSizeChange() {
this.prePage.current = 1;
this.loadPreReferences();
},
async openReference() {
if (this.form.paymentType === 'project_advance') return;
// 进度预付仅预结算;尾款付款默认正式结算,也可切到预结算
this.referenceTab = this.canSelectFormalSettlement ? 'formal' : 'pre';
this.referenceVisible = true;
this.formalSelection = [];
this.preSelection = [];
this.formalPage.current = 1;
this.prePage.current = 1;
await this.loadSettlementReferences();
},
async openSettlementDetail(row = {}) {
const settlementNo = String(row.settlementNo || '').trim();
const isPre =
row.referenceType === 'pre' ||
this.form.paymentType === 'progress_advance' ||
Boolean(this.form.preSettlementId || this.form.preSettlementNo);
let id =
row.id ||
row.preSettlementId ||
row.settlementId ||
(isPre
? this.form.preSettlementId || this.form.preSettlementIds?.[0]
: this.form.settlementId || this.form.settlementIds?.[0]);
if (!id && settlementNo) {
const matched = this.referenceRows.find(
item => String(item.settlementNo || '').trim() === settlementNo
);
id = matched?.id;
}
if (!id && settlementNo) {
try {
const listApi = isPre ? preApi.getList : formalApi.getList;
const params = isPre
? { preSettlementNo: settlementNo }
: { formalSettlementNo: settlementNo };
const data = this.unwrapData(await listApi(1, 10, params));
const matched = (data.records || []).find(item => {
const no = String(
item.preSettlementNo || item.formalSettlementNo || item.settlementNo || ''
).trim();
return no === settlementNo;
});
id = matched?.id;
} catch (error) {
this.$message.error('结算单详情加载失败');
return;
}
}
if (!id) {
this.$message.warning(isPre ? '未找到对应的预付结算单' : '未找到对应的付款结算单');
return;
}
this.$router.push({
path: isPre ? '/settlement/pre-settlement/form' : '/settlement/formal-settlement/form',
query: {
mode: 'view',
id,
name: isPre ? '查看预付结算单' : '查看付款结算单',
},
});
},
async selectContract(row) {
this.referenceRows = [];
this.form.invoices = [];
this.paymentRecords = [];
this.clearReceiptAccount();
Object.assign(this.form, {
projectId: row.projectId,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractId: row.id,
contractNo: row.contractNo,
contractName: row.contractName,
payerName: row.payerName || row.partyA || '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: row.settlementMode || '',
});
this.referenceVisible = false;
await Promise.all([
this.fillBillTypeFromContract(row.id, true),
this.loadAttachmentRuleData([]),
]);
if (this.billPayment) this.loadBillOptions();
},
async selectFormal(row) {
const validReferenceId = this.isValidReferenceId(row.id);
const [amountResponse, detailResponse] = await Promise.all([
validReferenceId
? api.getReferenceAmount('settlement_payment', row.id, this.form.id)
: Promise.resolve(null),
validReferenceId ? formalApi.getDetail(row.id) : Promise.resolve(null),
]);
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,
paymentSequence: amount.paymentSequence,
}),
];
this.clearReceiptAccount();
Object.assign(this.form, {
paymentType: 'settlement_payment',
settlementId: row.id,
settlementIds: [row.id],
preSettlementId: null,
preSettlementIds: [],
settlementNo: row.formalSettlementNo,
preSettlementNo: '',
projectId: row.projectId,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractId: row.contractId,
contractNo: row.contractNo,
contractName: row.contractName,
settlementAmount,
payableAmount,
billType: '',
payerName: row.payerName,
payeeName: '',
});
this.rememberSettlementParties(row.payerName, row.payeeName);
this.referenceVisible = false;
this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse);
this.form.invoices = this.createInvoiceRows(
detail?.invoices,
detail?.formalSettlementNo || row.formalSettlementNo
);
this.paymentRecords = [];
await Promise.all([
this.fillBillTypeFromContract(row.contractId, true),
this.loadAttachmentRuleData(detail?.id ? [detail] : []),
]);
if (this.billPayment) this.loadBillOptions();
},
confirmReferenceSelection() {
if (this.referenceTab === 'formal') return this.confirmFormalSelection();
return this.confirmPreSelection();
},
async confirmFormalSelection() {
const rows = this.formalSelection || [];
if (!rows.length) return;
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
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)));
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) {
const validReferenceId = this.isValidReferenceId(row.id);
// 尾款付款选择预结算时保留尾款付款类型
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
const [amountResponse, detailResponse] = await Promise.all([
validReferenceId
? api.getReferenceAmount(paymentType, row.id, this.form.id)
: Promise.resolve(null),
validReferenceId ? preApi.getDetail(row.id) : Promise.resolve(null),
]);
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,
referenceType: 'pre',
paymentSequence: amount.paymentSequence,
}),
];
this.form.invoices = [];
this.paymentRecords = [];
this.clearReceiptAccount();
Object.assign(this.form, {
paymentType,
preSettlementId: row.id,
preSettlementIds: [row.id],
settlementId: null,
settlementIds: [],
preSettlementNo: row.preSettlementNo,
settlementNo: '',
projectId: row.projectId,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractId: row.contractId,
contractNo: row.contractNo,
contractName: row.contractName,
settlementAmount,
payableAmount,
billType: '',
payerName: row.payerName,
payeeName: '',
});
this.rememberSettlementParties(row.payerName, row.payeeName);
this.referenceVisible = false;
this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse);
await Promise.all([
this.fillBillTypeFromContract(row.contractId, true),
this.loadAttachmentRuleData(detail?.id ? [detail] : []),
]);
if (this.billPayment) this.loadBillOptions();
},
async confirmPreSelection() {
const rows = this.preSelection || [];
if (!rows.length) return;
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
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)));
const detailRows = details.map((response, index) => ({
...validRows[index],
...this.unwrapData(response),
}));
await this.applyTransferredPreSettlements(detailRows);
await this.loadAttachmentRuleData(detailRows);
this.referenceVisible = false;
},
normalizeAttachments(files) {
const existingAttachments = this.form.attachments || [];
const userInfo = this.$store.getters.userInfo || {};
const uploadUserName = userInfo.realName || userInfo.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => {
const fileUrl = this.attachmentFileUrl(file);
const existingFile = existingAttachments.find(item => {
const sameUid = file.uid && item.uid && String(file.uid) === String(item.uid);
const sameUrl = fileUrl && this.attachmentFileUrl(item) === fileUrl;
return sameUid || sameUrl;
});
return {
...existingFile,
...file,
attachmentType:
existingFile?.attachmentType || this.resolveAttachmentTypeByFileName(file),
description: file.description || existingFile?.description || '',
uploadUserName: existingFile?.uploadUserName || file.uploadUserName || uploadUserName,
uploadTime: existingFile?.uploadTime || file.uploadTime || uploadTime,
};
});
},
formatAttachmentSize(file = {}) {
const rawSize = file.size ?? file.fileSize ?? file.attachSize;
if (rawSize === undefined || rawSize === null || rawSize === '') return '-';
const matched = String(rawSize)
.trim()
.match(/^([\d.]+)\s*(B|K|KB|M|MB|G|GB)?$/i);
if (!matched) return '-';
const value = Number(matched[1]);
if (!Number.isFinite(value)) return '-';
const unit = (matched[2] || 'B').toUpperCase();
const unitToMb = {
B: 1 / 1024 / 1024,
K: 1 / 1024,
KB: 1 / 1024,
M: 1,
MB: 1,
G: 1024,
GB: 1024,
};
return `${(value * unitToMb[unit]).toFixed(2)} MB`;
},
handleAttachmentBatchDownload() {
this.downloadableAttachments.forEach((file, index) => {
window.setTimeout(
() => downloadFileByUrl(this.attachmentFileUrl(file), this.attachmentFileName(file)),
index * 500
);
});
},
rowPaymentRatioMax(row) {
const settlementAmount = Number(row.settlementAmount || 0);
const payableAmount = Number(row.payableAmount || 0);
if (settlementAmount <= 0) return 100;
return Number(Math.min(100, (payableAmount / settlementAmount) * 100).toFixed(2));
},
/**
* 结算行金额规范化:只在行内反算比例,不使用付款基本信息的比例和申请金额。
*/
seedMultipleRowPayments(rows = []) {
rows.forEach(item => {
if (this.nullableNumber(item.appliedAmount) === null && this.nullableNumber(item.paymentRatio) === null) {
item.appliedAmount = null;
item.paymentRatio = null;
return;
}
this.syncRowPayment(item, 'amount');
});
},
/**
* 结算行付款比例与付款金额联动。比例 = 付款金额 / 结算总金额。
* @param {'ratio'|'amount'} source 以哪一侧为准
*/
syncRowPayment(row, source) {
const settlementAmount = Number(row.settlementAmount || 0);
const payableAmount = Number(row.payableAmount || 0);
if (source === 'ratio') {
const ratio = this.nullableNumber(row.paymentRatio);
if (ratio === null) {
row.paymentRatio = null;
row.appliedAmount = null;
return;
}
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
row.appliedAmount = applied;
row.paymentRatio = settlementAmount
? Number(((applied / settlementAmount) * 100).toFixed(2))
: Number(ratio.toFixed(2));
return;
}
const appliedValue = this.nullableNumber(row.appliedAmount);
if (appliedValue === null) {
row.appliedAmount = null;
row.paymentRatio = null;
return;
}
let applied = appliedValue;
if (applied < 0) applied = 0;
if (applied > payableAmount) applied = payableAmount;
row.appliedAmount = Number(applied.toFixed(2));
row.paymentRatio = settlementAmount
? Number(((row.appliedAmount / settlementAmount) * 100).toFixed(2))
: 0;
},
findReferenceRow(row) {
return this.referenceRows.find(
item =>
item.id === row.id &&
item.referenceType === row.referenceType &&
item.settlementNo === row.settlementNo
);
},
handleRowPaymentRatioChange(row, value) {
const target = this.findReferenceRow(row);
if (!target) return;
target.paymentRatio = value;
this.syncRowPayment(target, 'ratio');
this.syncHeaderSettlementTotalsFromRows();
},
handleRowAppliedAmountChange(row, value) {
const target = this.findReferenceRow(row);
if (!target) return;
target.appliedAmount = value;
this.syncRowPayment(target, 'amount');
this.syncHeaderSettlementTotalsFromRows();
},
/** 多结算单时仅同步结算金额/可付款金额合计,申请付款金额与比例由上方独立维护 */
syncHeaderSettlementTotalsFromRows() {
if (this.referenceRows.length <= 1) return;
const settlementAmount = this.referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0),
0
);
const payableAmount = this.referenceRows.reduce(
(total, item) => total + Number(item.payableAmount || 0),
0
);
this.amountSyncing = true;
this.form.settlementAmount = Number(settlementAmount.toFixed(2));
this.form.payableAmount = Number(payableAmount.toFixed(2));
this.$nextTick(() => {
this.amountSyncing = false;
this.$refs.formRef?.validateField?.(['settlementAmount', 'appliedAmount']);
});
},
/** @deprecated 兼容旧调用名,行为见 syncHeaderSettlementTotalsFromRows */
syncHeaderPaymentFromRows() {
this.syncHeaderSettlementTotalsFromRows();
},
payload() {
// 驳回重提前再清一次:避免历史误存的正式结算单 ID 盖过预结算引用
this.clearFormalReferenceWhenPreOnly();
const {
id,
paymentType,
settlementId,
settlementIds,
preSettlementId,
preSettlementIds,
projectId,
projectName,
deptId,
deptName,
contractId,
contractNo,
contractName,
payerName,
payeeName,
settlementAmount,
payableAmount,
billType,
paymentRatio,
appliedAmount,
paymentMethod,
billLedgerId,
billNo,
receiptAccountId,
receiptAccountName,
bankName,
bankAccount,
attachments,
remark,
invoices,
} = this.form;
const usePreSettlementOnly =
paymentType === 'progress_advance' ||
(this.referenceRows.length > 0 &&
this.referenceRows.every(item => item.referenceType === 'pre'));
// 进度预付 / 仅预结算引用:强制不传正式结算单 ID,并兜底恢复预结算单 ID
const resolvedPreSettlementId =
preSettlementId ||
preSettlementIds?.[0] ||
(usePreSettlementOnly ? settlementId : null) ||
null;
const resolvedPreSettlementIds =
preSettlementIds?.length > 0
? preSettlementIds
: resolvedPreSettlementId
? [resolvedPreSettlementId]
: [];
const preSettlementNo = usePreSettlementOnly
? this.form.preSettlementNo ||
(this.referenceRows || [])
.filter(item => item.referenceType === 'pre')
.map(item => item.settlementNo)
.filter(Boolean)
.join('、')
: this.form.preSettlementNo;
return {
id,
paymentType,
settlementId: usePreSettlementOnly ? null : settlementId,
settlementIds: usePreSettlementOnly ? [] : settlementIds,
preSettlementId: usePreSettlementOnly ? resolvedPreSettlementId : preSettlementId,
preSettlementIds: usePreSettlementOnly ? resolvedPreSettlementIds : preSettlementIds,
preSettlementNo,
projectId,
projectName,
deptId,
deptName,
contractId,
contractNo,
contractName,
payerName,
payeeName,
settlementAmount,
payableAmount,
billType,
paymentRatio,
appliedAmount,
settlementLines: (this.referenceRows || []).map(item => ({
formalSettlementId: item.referenceType === 'formal' ? item.id : null,
preSettlementId: item.referenceType === 'pre' ? item.id : null,
paymentRatio: this.nullableNumber(item.paymentRatio),
appliedAmount: this.nullableNumber(item.appliedAmount),
})),
paymentMethod,
billLedgerId,
billNo,
receiptAccountId,
receiptAccountName,
bankName,
bankAccount,
attachmentsJson: JSON.stringify(attachments || []),
remark,
invoices:
paymentType === 'project_advance' || paymentType === 'progress_advance'
? []
: invoices.map(invoice => ({
settlementNo: invoice.settlementNo || '',
invoiceNo: invoice.invoiceNo || '',
invoiceDate: invoice.invoiceDate || null,
invoiceType: invoice.invoiceType || '',
taxRate: Number(invoice.taxRate || 0),
invoiceAmount: Number(invoice.invoiceAmount || 0),
matchedAmount: Number(invoice.matchedAmount || 0),
attachmentJson: invoice.attachmentJson || '',
})),
paymentRecords:
paymentType === 'project_advance'
? []
: this.paymentRecords.map(record => ({
paidAmount: Number(record.paidAmount || 0),
paidDate: this.formatLocalDate(record.paidDate),
paymentNo: record.paymentNo || '',
voucherJson: record.voucherJson || '',
kingdeeBillNo: record.kingdeeBillNo || '',
})),
};
},
/** 顶部「保存」:保存成功后关闭表单页并回到列表 */
async handleSave() {
await this.saveDraft({ showSuccess: true, closePage: true });
},
clearMessages() {
ElMessage.closeAll();
this.$message?.closeAll?.();
},
async saveDraft(options = {}) {
// 仅显式 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.isSettlementRelatedType && this.referenceRows.length) {
const invalidRow = this.referenceRows.find(item => {
const rowApplied = Number(item.appliedAmount || 0);
return !Number.isFinite(rowApplied) || rowApplied <= 0;
});
if (invalidRow) {
this.clearMessages();
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}请填写付款金额,且必须大于0`);
return false;
}
const exceededRow = this.referenceRows.find(
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
);
if (exceededRow) {
this.clearMessages();
ElMessage.warning(`${exceededRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
return false;
}
}
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.clearMessages();
ElMessage.warning('请选择所属项目');
return false;
}
try {
this.validateInvoices();
this.validatePaymentRecords();
} catch (error) {
this.clearMessages();
ElMessage.warning(error.message);
return false;
}
const data = this.unwrapData(await api.save(this.payload()));
this.form.id = data;
if (showSuccess) {
this.clearMessages();
ElMessage.success('保存成功');
}
if (closePage) {
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
this.goBack();
}
return true;
},
async submitForm() {
// 提交前静默保存;不校验/提示附件未上传;全程只保留一条「提交成功」
this.clearMessages();
const saved = await this.saveDraft({ showSuccess: false, closePage: false });
if (!saved) return;
// 高风险/黑名单收款方必须上传特批附件(与后端提交强校验同口径)
if (
this.missingAttachmentMaterials.some(
item => item.key === ATTACHMENT_MATERIALS.specialApproval.key
)
) {
this.$message.warning('收款方为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批');
return;
}
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'payment-application',
bizId: this.form.id,
subjectName: this.form.paymentNo || '',
approvalStatus: previousApprovalStatus,
mkEnabled: await loadMkEnabled().catch(() => false),
});
this.clearMessages();
ElMessage.success('提交成功');
// 通知列表页(keep-alive)重新拉取列表
sessionStorage.setItem(paymentApplicationListRefreshKey, '1');
this.goBack();
},
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter?.closeTag?.();
this.$router.push(paymentApplicationListRoute);
},
formatMoney(value) {
const amount = Number(value);
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00';
},
},
};
</script>
<style scoped lang="scss">
.payment-form-page__form {
padding-bottom: 72px;
}
// 风险客商提示条(基本信息备注后,同合同新增页):橙黄背景 + 红色文字
.payment-form-page__risk-tip {
margin: 0 0 18px;
padding: 10px 16px;
background: #faecd8;
border-radius: 4px;
.payment-form-page__risk-tip-title {
color: #f56c6c;
font-weight: 600;
line-height: 22px;
}
.payment-form-page__risk-tip-content {
color: #f56c6c;
line-height: 22px;
}
}
.payment-form-page__settlement-nos {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-height: 32px;
}
.payment-form-page__settlement-input {
flex: 1;
min-width: 0;
}
.payment-form-page__settlement-input :deep(.el-input__inner) {
cursor: pointer;
}
.payment-form-page__settlement-input.is-disabled :deep(.el-input__inner) {
cursor: not-allowed;
}
.payment-form-page__settlement-select-btn {
flex-shrink: 0;
}
.payment-form-page__reference-search {
padding: 12px 12px 4px;
margin-bottom: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.payment-form-page__reference-search-fields {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 16px;
}
.payment-form-page__reference-search :deep(.el-form-item) {
margin-bottom: 8px;
}
.payment-form-page__reference-search :deep(.el-form-item__label) {
white-space: nowrap;
}
.payment-form-page__reference-search-actions,
.payment-form-page__reference-pagination,
.payment-form-page__reference-footer {
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;
}
.payment-form-page__actions {
display: flex;
justify-content: flex-end;
padding: 12px 24px;
position: fixed;
right: 0;
left: 230px;
bottom: 0;
margin: 0;
z-index: 10;
background: #fff;
border-top: 1px solid #eff1f7;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
:global(.avue--collapse .payment-form-page__actions) {
left: 60px;
}
:global(.avue-layout--horizontal .payment-form-page__actions) {
left: 0;
}
.payment-form-page__attachment-upload {
display: flex;
justify-content: flex-start;
margin-top: 12px;
}
.payment-form-page__missing-text {
margin-left: 12px;
color: var(--el-color-danger);
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;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.payment-form-page__attachment-download {
margin-left: auto;
}
.payment-form-page__invoice-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.payment-form-page__attachment-upload .vehicle-attachment-upload {
width: auto;
}
.payment-form-page__attachment-upload .el-upload {
display: inline-flex;
}
.payment-form-page :deep(.el-form-item) {
margin-bottom: 24px;
}
.payment-form-page :deep(.el-input-number .el-input__inner) {
text-align: left;
}
.payment-form-page__percentage-input {
display: flex;
width: 100%;
}
.payment-form-page__percentage-input :deep(.el-input-number) {
flex: 1;
width: 0;
}
.payment-form-page__percentage-input :deep(.el-input__wrapper) {
border-radius: var(--el-border-radius-base) 0 0 var(--el-border-radius-base);
}
.payment-form-page__percentage-append {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
padding: 0 16px;
color: var(--el-text-color-regular);
background-color: var(--el-fill-color-light);
border: 1px solid var(--el-border-color);
border-left: 0;
border-radius: 0 var(--el-border-radius-base) var(--el-border-radius-base) 0;
}
.payment-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.payment-form-page :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
:deep(.payment-form-page.basic-container .basic-container__card > .el-card__body) {
padding: 0;
}
</style>