Files
tms-erp-web/src/views/payment/payment-application-form.vue
T
b2894lxlx 6212d68a77 ✨ 付款申请表单优化与结算单过滤,并完善相关体验
进度预付仅可选预结算、尾款付款可选正式/预结算;同步收款账户、申请金额校验、关闭标签与费用明细残留等交互,并补充运单复制需求说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 00:17:07 +08:00

2826 lines
102 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">
<template v-if="referenceSettlementLinks.length">
<el-link
v-for="item in referenceSettlementLinks"
:key="`${item.id || item.settlementNo}`"
type="primary"
@click="openSettlementDetail(item)"
>
{{ item.settlementNo }}
</el-link>
</template>
<el-input
v-else
v-model="referenceLabel"
readonly
:disabled="readonly"
placeholder="请选择预结算/正式结算单"
/>
<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="item.contractName || item.contractNo"
: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="0"
/></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>
</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="110">
<template #default="{ row }">{{ formatMoney(row.paymentRatio) }}</template>
</el-table-column>
<el-table-column prop="appliedAmount" label="付款金额" min-width="130">
<template #default="{ row }">{{ formatMoney(row.appliedAmount) }}</template>
</el-table-column>
</el-table>
</section-card>
<section-card v-if="isSettlementRelatedType" 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" @click="viewInvoiceAttachment(row)">查看</el-link>
<el-link type="primary" @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"
><template #default="{ row }"
><div
class="payment-form-page__attachment-file-name-cell"
:title="attachmentFileName(row) || '-'"
>
<span
class="payment-form-page__attachment-file-name"
:class="{ 'is-disabled': !attachmentFileUrl(row) }"
@click="attachmentFileUrl(row) && previewAttachment(row)"
>{{ attachmentFileName(row) || '-' }}</span
>
</div></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="saveDraft()"
>保存</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>
<div 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>
</div>
<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="结算单号" />
<el-table-column prop="projectName" label="所属项目" />
<el-table-column prop="contractName" label="合同名称" />
<el-table-column prop="settlementAmount" label="结算金额">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</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="结算单号" />
<el-table-column prop="projectName" label="所属项目" />
<el-table-column prop="contractName" label="合同名称" />
<el-table-column prop="settlementAmount" label="结算金额">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</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">
<el-form :model="billQuery" label-position="right" label-width="160px" @submit.prevent>
<div class="payment-form-page__reference-search-fields">
<el-form-item label="汇票筛选">
<el-input
v-model="billQuery.keyword"
clearable
placeholder="请输入汇票号、出票单位或收票单位"
@keyup.enter="handleBillSearch"
/>
</el-form-item>
</div>
<div class="payment-form-page__reference-search-actions">
<el-button type="primary" @click="handleBillSearch">搜索</el-button>
<el-button @click="resetBillSearch">清空</el-button>
</div>
</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 } 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 * 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 { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import PdfPreview from '@/components/pdf-preview/main.vue';
const attachmentViewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
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,
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,
billQuery: {
keyword: '',
},
billRows: [],
billPage: {
current: 1,
size: 10,
total: 0,
loading: false,
},
receiptAccountOptions: [],
receiptAccountLoading: false,
contractPartyBName: '',
firstContractPayment: false,
contractSignatureFileAvailable: false,
contractPaymentRatioLimit: null,
contractPaymentRatioExceeded: false,
payeeCustomerLevel: '',
attachmentRuleLoading: false,
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' },
],
},
};
},
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);
},
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',
}));
},
settlementRows() {
if (this.referenceRows.length) {
return this.referenceRows.map(item => ({
...item,
paymentRatio: this.form.paymentRatio,
appliedAmount:
item.fixedAppliedAmount === true
? Number(item.appliedAmount || 0)
: Number(
(
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
100
).toFixed(2)
),
}));
}
if (!this.referenceLabel) return [];
return [
{
settlementNo: this.referenceLabel,
settlementAmount: this.form.settlementAmount,
payableAmount: this.form.payableAmount,
paymentRatio: this.form.paymentRatio,
appliedAmount: this.form.appliedAmount,
},
];
},
highRiskPayee() {
const level = String(this.payeeCustomerLevel || '')
.trim()
.toUpperCase();
return ['D', 'D级', 'E', 'E级', 'F', 'F级'].includes(level) || level.includes('高风险');
},
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
) {
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: {
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) {
const amount = Number(value);
const payableAmount = Number(this.form.payableAmount || 0);
if (value === null || value === undefined || value === '' || !Number.isFinite(amount)) {
callback(new Error('请填写申请付款金额'));
return;
}
if (amount <= 0) {
callback(new Error('申请付款金额必须大于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 &&
Number(this.form.appliedAmount || 0) > Number(selected.availableBalance || 0)
) {
callback(new Error('申请付款金额不能超过汇票可用余额'));
return;
}
callback();
},
validateInvoices() {
if (this.form.paymentType === 'project_advance') 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 = referenceRow.settlementNo ? [referenceRow] : [];
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 = referenceRow.settlementNo ? [referenceRow] : [];
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.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 = [];
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();
await this.loadReceiptAccountOptions(autoSelectDefault);
} catch (error) {
this.form.billType = '';
this.contractPartyBName = '';
this.receiptAccountOptions = [];
}
},
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 referenceRows = rows.map(item => ({
...this.createReferenceRow(item),
settlementNo: item.formalSettlementNo || item.settlementNo || '',
appliedAmount: Number(item.appliedAmount || 0),
fixedAppliedAmount: true,
}));
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 appliedAmount = referenceRows.reduce(
(total, item) => total + Number(item.appliedAmount || 0),
0
);
const paymentRatio = settlementAmount
? Number(((appliedAmount / settlementAmount) * 100).toFixed(2))
: 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: '',
paymentRatio,
appliedAmount: Number(appliedAmount.toFixed(2)),
invoices: rows.flatMap(item =>
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
),
});
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();
});
},
createReferenceRow(row) {
const settlementAmount = Number(row.settlementAmount || 0);
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
const isPre = Boolean(row.preSettlementNo || row.preSettlementId || row.referenceType === 'pre');
return {
id: row.preSettlementId || row.settlementId || row.id,
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
referenceType: isPre ? 'pre' : 'formal',
settlementAmount,
payableAmount:
row.payableAmount === undefined || row.payableAmount === null
? Math.max(0, settlementAmount - appliedAmount)
: Number(row.payableAmount || 0),
};
},
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 = [];
this.referenceRows = rows.map(item =>
this.createReferenceRow({ ...item, referenceType: 'pre' })
);
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
);
const paymentRatio = Number(this.form.paymentRatio || 0);
// 尾款付款场景下选择预结算单时保留尾款付款类型,不强制改为进度预付
const paymentType =
this.form.paymentType === 'settlement_payment' ? 'settlement_payment' : 'progress_advance';
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: Number(((settlementAmount * paymentRatio) / 100).toFixed(2)),
});
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`
);
},
viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row);
if (!this.attachmentFileUrl(attachment)) {
this.$message.warning('当前发票暂无可查看附件');
return;
}
this.previewAttachment(
{ ...attachment, name: this.invoiceAttachmentName(row) },
[attachment]
);
},
downloadInvoiceAttachment(row) {
const url = this.invoiceAttachmentUrl(row);
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 typeMatched = (materials || []).find(
material => file.attachmentType === MATERIAL_TYPE_MAP[material.key]
);
return typeMatched || this.matchAttachmentMaterialByFileName(file, materials);
},
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null) {
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 => item.material && !existingUrls.has(this.attachmentFileUrl(item.file)))
.map(({ file, material }) => {
existingUrls.add(this.attachmentFileUrl(file));
return {
...file,
originalName: this.attachmentFileName(file),
name: this.attachmentFileName(file),
url: this.attachmentFileUrl(file),
link: file.link || this.attachmentFileUrl(file),
attachmentType: MATERIAL_TYPE_MAP[material.key],
description: file.description || `自动取自${sourceName}`,
sourceImported: true,
};
});
if (imported.length) this.form.attachments = [...this.form.attachments, ...imported];
},
async loadCurrentSettlementDetails() {
if (
this.form.paymentType === 'progress_advance' &&
this.isValidReferenceId(this.form.preSettlementId)
) {
const response = await preApi.getDetail(this.form.preSettlementId);
const detail = this.unwrapData(response);
return detail?.id ? [detail] : [];
}
if (
this.form.paymentType === 'settlement_payment' &&
this.isValidReferenceId(this.form.settlementId)
) {
const response = await formalApi.getDetail(this.form.settlementId);
const detail = this.unwrapData(response);
return detail?.id ? [detail] : [];
}
return [];
},
async loadAttachmentRuleData(settlementRows = []) {
this.attachmentRuleLoading = true;
this.firstContractPayment = false;
this.contractSignatureFileAvailable = false;
this.resetContractPaymentRatioLimit();
try {
const settlementMaterials = [
ATTACHMENT_MATERIALS.weighingSlip,
ATTACHMENT_MATERIALS.entrustOrder,
ATTACHMENT_MATERIALS.settlement,
];
const settlementFiles = (settlementRows || []).flatMap(row =>
this.parse(row.attachmentsJson)
);
this.mergeAutomaticAttachments(settlementFiles, settlementMaterials, '结算单');
if (!this.form.contractId) {
this.firstContractPayment = false;
return;
}
const contractResponse = await getContractDetail(this.form.contractId);
const contract = this.unwrapData(contractResponse) || {};
if (this.form.paymentType === 'project_advance') {
this.applyContractPaymentRatioLimit(contract);
const contractFiles = this.parse(contract.contractFileJson);
this.contractSignatureFileAvailable = contractFiles.some(file =>
Boolean(this.attachmentFileUrl(file))
);
this.mergeAutomaticAttachments(
contractFiles,
[ATTACHMENT_MATERIALS.contract],
'合同',
ATTACHMENT_MATERIALS.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'
);
if (this.firstContractPayment) {
this.mergeAutomaticAttachments(
this.parse(contract.contractFileJson),
[ATTACHMENT_MATERIALS.contract],
'合同',
ATTACHMENT_MATERIALS.contract
);
}
} catch (error) {
this.$message.warning('附件清单来源材料加载失败,请核对后手工补充');
} finally {
this.attachmentRuleLoading = false;
}
},
resetContractPaymentRatioLimit() {
this.contractPaymentRatioLimit = null;
this.contractPaymentRatioExceeded = false;
},
applyContractPaymentRatioLimit(contract = {}) {
const [firstRatio] = this.parse(contract.paymentRatioJson);
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 = '';
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 || '';
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) {
const name = item.accountName || item.accountHolderName || '未命名收款方';
return item.bankAccount ? `${name}(${item.bankAccount})` : name;
},
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: project?.undertakeDeptId || project?.businessDeptId || null,
deptName: project?.undertakeDeptName || 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, this.form.deptId, selectedId)
) || [];
} 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,
this.form.deptId,
this.form.billLedgerId
);
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();
},
selectBill(row) {
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 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) || {};
this.formalRows = data.records || [];
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) || {};
this.preRows = data.records || [];
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 })];
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.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)));
await this.applyTransferredFormalSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
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' }),
];
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.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)));
await this.applyTransferredPreSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
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
);
});
},
payload() {
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;
return {
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,
attachmentsJson: JSON.stringify(attachments || []),
remark,
invoices:
paymentType === 'project_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 saveDraft(options = {}) {
const showSuccess = options.showSuccess !== false;
await this.$refs.formRef.validate();
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.$message.warning('请选择所属项目');
return false;
}
try {
this.validateInvoices();
this.validatePaymentRecords();
} catch (error) {
this.$message.warning(error.message);
return false;
}
const data = this.unwrapData(await api.save(this.payload()));
this.form.id = data;
if (showSuccess) {
this.$message.success('保存成功');
}
return true;
},
async submitForm() {
// 提交前静默保存,避免同时弹出「保存成功」与材料缺失 toast
const saved = await this.saveDraft({ showSuccess: false });
if (!saved) return;
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'payment-application',
formInstanceId: this.form.id,
subjectName: this.form.paymentNo || '',
approvalStatus: this.form.approvalStatus || '',
});
this.$message.success('提交成功');
this.goBack();
},
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/payment/payment-application');
},
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__settlement-nos {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
width: 100%;
min-height: 32px;
}
.payment-form-page__settlement-nos :deep(.el-input) {
flex: 1;
min-width: 120px;
}
.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__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__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-file-name-cell {
display: block;
width: 100%;
min-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.payment-form-page__attachment-file-name {
display: block;
width: 100%;
overflow: hidden;
color: var(--el-text-color-regular);
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s ease;
&:hover {
color: #409eff;
}
&.is-disabled {
color: var(--el-text-color-regular);
cursor: default;
&:hover {
color: var(--el-text-color-regular);
}
}
}
.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: 14px;
}
.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>