Files
tms-erp-web/src/views/payment/payment-application-form.vue
T
2026-08-28 16:01:42 +08:00

2286 lines
82 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" @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="form.paymentType !== 'project_advance'" :span="6"
><el-form-item label="结算单号" prop="referenceId"
><el-input
v-model="referenceLabel"
readonly
:disabled="readonly"
placeholder="请选择预结算/正式结算单"
><template #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></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="form.paymentType !== 'project_advance'" :span="6"
><el-form-item label="结算金额" prop="settlementAmount"
><el-input-number
v-model="form.settlementAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col v-if="form.paymentType !== 'project_advance'" :span="6"
><el-form-item label="可付款金额"
><el-input v-model="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"
:controls="false"
style="width: 100%" /></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-select
v-model="form.billLedgerId"
:disabled="readonly"
filterable
remote
reserve-keyword
:remote-method="loadBillOptions"
:loading="billLoading"
placeholder="请选择可用汇票"
@change="handleBillChange"
><el-option
v-for="item in billOptions"
:key="item.id"
:label="`${item.billNo}|余额${formatMoney(item.availableBalance)}${
item.maturityDate
}`"
:value="item.id" /></el-select></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="收款方"
><el-input v-model="form.payeeName" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="收款账号" prop="receiptAccountId"
><el-select
v-model="form.receiptAccountId"
:disabled="readonly || !form.payeeName"
:loading="receiptAccountLoading"
filterable
clearable
placeholder="请选择收款方的收款账号"
no-data-text="该收款方未维护可用收款信息"
@change="handleReceiptAccountChange"
><el-option
v-for="item in receiptAccountOptions"
:key="item.id"
:label="receiptAccountLabel(item)"
:value="item.id" /></el-select></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="开户银行"
><el-input v-model="form.bankName" disabled /></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="form.paymentType !== 'project_advance'" title="结算信息">
<el-table :data="settlementRows" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="settlementNo"
label="结算单号"
min-width="150" /><el-table-column
prop="settlementAmount"
label="结算总金额"
min-width="130" /><el-table-column
prop="payableAmount"
label="可付款金额"
min-width="130" /><el-table-column
prop="paymentRatio"
label="付款比例(%"
min-width="110" /><el-table-column
prop="appliedAmount"
label="付款金额"
min-width="130"
/></el-table>
</section-card>
<section-card v-if="form.paymentType !== 'project_advance'" title="发票信息">
<el-table :data="form.invoices" border>
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="settlementNo" label="结算单号" min-width="120" />
<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 v-if="!readonly" label="操作" width="100">
<template #default="{ $index }">
<el-link type="danger" @click="form.invoices.splice($index, 1)">删除</el-link>
</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"
@success="handleManualAttachmentUpload"
/>
</div>
</section-card>
<div class="payment-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft(false)"
>保存</el-button
>
<el-button
v-if="!readonly && canSave && hasPermission('payment_application_submit')"
type="primary"
@click="submitForm"
>提交</el-button
>
</div>
</el-form>
<el-dialog
v-model="referenceVisible"
:title="form.paymentType === 'project_advance' ? '选择项目合同' : '选择结算单'"
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 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="结算金额" />
<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-confirm">
<el-button type="primary" :disabled="!formalSelection.length" @click="confirmFormalSelection">确定</el-button>
</div>
<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 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="结算金额" />
<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>
<div class="payment-form-page__reference-confirm">
<el-button type="primary" :disabled="!preSelection.length" @click="confirmPreSelection">确定</el-button>
</div>
</el-tab-pane>
</el-tabs></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 * as billLedgerApi from '@/api/payment/billLedger';
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 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: 'project_advance',
settlementId: null,
settlementIds: [],
preSettlementId: null,
preSettlementIds: [],
projectId: null,
projectName: '',
deptId: null,
deptName: '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
paymentRatio: 50,
appliedAmount: 0,
paymentMethod: 'bank_transfer',
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,
receiptAccountOptions: [],
receiptAccountLoading: false,
firstContractPayment: false,
contractSignatureFileAvailable: 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: api.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: [{ 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: [{ validator: this.validateAppliedAmount, trigger: 'change' }],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
canSave() {
const code = this.recordId ? 'payment_application_edit' : 'payment_application_add';
return this.hasPermission(code);
},
billPayment() {
return this.form.paymentMethod !== 'bank_transfer';
},
paymentAmountBase() {
if (this.form.paymentType === 'project_advance') return 0;
const amount = this.form.settlementAmount;
return Number(amount || 0);
},
referenceLabel() {
if (this.referenceRows.length) {
return this.referenceRows
.map(item => item.settlementNo)
.filter(Boolean)
.join('、');
}
return this.form.settlementNo || this.form.preSettlementNo || '';
},
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) {
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.form.paymentType === 'progress_advance' &&
!this.form.preSettlementId &&
!this.form.preSettlementIds?.length
) {
callback(new Error('请选择预结算单'));
return;
}
if (this.form.paymentType === 'settlement_payment' && !this.form.settlementId) {
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 (!Number.isFinite(amount) || 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 initialize() {
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.loadReceiptAccountOptions(false);
await this.loadAttachmentRuleData(await this.loadCurrentSettlementDetails());
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
} else {
this.form = emptyForm();
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
: 'project_advance';
},
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) {
return id !== null && id !== undefined && String(id).trim() !== '-1';
},
async loadTransferredPreSettlements() {
const fallbackRows = this.transferPayload?.sourcePreSettlements || [];
const ids = this.getTransferredPreSettlementIds();
if (!ids.length) {
this.applyTransferredPreSettlements(fallbackRows);
await Promise.all([
this.loadReceiptAccountOptions(),
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;
this.applyTransferredPreSettlements(sourceRows);
await Promise.all([
this.loadReceiptAccountOptions(),
this.loadAttachmentRuleData(sourceRows),
]);
} catch (error) {
if (fallbackRows.length) {
this.applyTransferredPreSettlements(fallbackRows);
await Promise.all([
this.loadReceiptAccountOptions(),
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;
}
})
);
this.applyTransferredFormalSettlements(rows.filter(item => this.isValidReferenceId(item?.id)));
await Promise.all([this.loadReceiptAccountOptions(), this.loadAttachmentRuleData(rows)]);
},
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: first.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.$nextTick(() => {
this.amountSyncing = false;
this.$refs.formRef?.clearValidate();
});
},
createReferenceRow(row) {
const settlementAmount = Number(row.settlementAmount || 0);
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
return {
id: row.preSettlementId || row.settlementId || row.id,
settlementNo: row.preSettlementNo || row.formalSettlementNo || row.settlementNo || '',
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 || '',
}));
},
createMockPaymentRecords(settlementNo, amount = this.form.appliedAmount) {
const totalCents = Math.max(0, Math.round(Number(amount || 0) * 100));
const paidCents = [Math.round(totalCents * 0.2), Math.round(totalCents * 0.3)];
paidCents.push(totalCents - paidCents[0] - paidCents[1]);
const normalizedSettlementNo = String(settlementNo || 'SETTLEMENT').slice(0, 60);
this.paymentRecords = paidCents.map((value, index) => {
const paidDate = this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD');
const sequence = String(index + 1).padStart(2, '0');
return {
paidAmount: Number((value / 100).toFixed(2)),
paidDate,
paymentNo: `MOCK-${normalizedSettlementNo}-${sequence}`,
voucherJson: '',
kingdeeBillNo: `MOCK-KD${paidDate.replaceAll('-', '')}${sequence}`,
};
});
},
referencePaymentAmount(settlementAmount) {
return Number(
((Number(settlementAmount || 0) * Number(this.form.paymentRatio || 0)) / 100).toFixed(2)
);
},
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));
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);
this.amountSyncing = true;
Object.assign(this.form, {
paymentType: 'progress_advance',
settlementId: null,
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: first.payeeName,
settlementAmount: Number(settlementAmount.toFixed(2)),
payableAmount: Number(payableAmount.toFixed(2)),
billType: '预结算单',
appliedAmount: Number(((settlementAmount * paymentRatio) / 100).toFixed(2)),
});
this.createMockPaymentRecords(this.form.preSettlementNo, this.form.appliedAmount);
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 [];
}
},
attachmentFileName(file) {
return String(file?.originalName || file?.name || file?.fileName || '').trim();
},
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) {
const url = this.attachmentFileUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isAttachmentImage(file)) {
this.attachmentImagePreviewUrls = (this.form.attachments || [])
.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.attachmentFileName(file).toLocaleLowerCase();
return (
Boolean(this.attachmentFileUrl(file)) &&
(file.attachmentType === MATERIAL_TYPE_MAP[material.key] ||
material.keywords.some(keyword => fileName.includes(keyword.toLocaleLowerCase())))
);
},
resolveAttachmentTypeByFileName(file) {
let fileName = this.attachmentFileName(file) || this.attachmentFileUrl(file);
fileName = String(fileName || '').split(/[?#]/)[0].toLocaleLowerCase();
try {
fileName = decodeURIComponent(fileName);
} catch {}
const material = [...Object.values(ATTACHMENT_MATERIALS)]
.sort(
(a, b) =>
Math.max(...b.keywords.map(String.length)) - Math.max(...a.keywords.map(String.length))
)
.find(item =>
item.keywords.some(keyword => fileName.includes(String(keyword).toLocaleLowerCase()))
);
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)) {
return [...materials]
.sort(
(a, b) =>
Math.max(...b.keywords.map(String.length)) - Math.max(...a.keywords.map(String.length))
)
.find(material => this.attachmentMatchesMaterial(file, material));
},
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 === 'project_advance') return [];
const requests = [];
if (this.isValidReferenceId(this.form.preSettlementId)) {
requests.push(preApi.getDetail(this.form.preSettlementId));
}
if (this.isValidReferenceId(this.form.settlementId)) {
requests.push(formalApi.getDetail(this.form.settlementId));
}
if (!requests.length) return [];
const responses = await Promise.all(requests);
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
},
async loadAttachmentRuleData(settlementRows = []) {
this.attachmentRuleLoading = true;
this.firstContractPayment = false;
this.contractSignatureFileAvailable = false;
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') {
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;
}
},
validateRequiredAttachments() {
if (!this.missingAttachmentMaterials.length) return true;
this.$message.warning(
`请先上传付款申请所需材料:${this.missingAttachmentMaterials
.map(item => item.label)
.join('、')}`
);
return false;
},
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.clearReceiptAccount();
this.loadAttachmentRuleData([]);
if (this.form.paymentType === 'project_advance') this.loadProjectOptions();
},
clearReceiptAccount() {
Object.assign(this.form, {
receiptAccountId: null,
receiptAccountName: '',
bankName: '',
bankAccount: '',
});
},
customerRecords(response) {
return this.unwrapData(response)?.records || [];
},
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)
);
if (
this.form.contractId &&
this.form.contractName &&
!this.contractOptions.some(item => String(item.id) === String(this.form.contractId))
) {
this.contractOptions.unshift({
id: this.form.contractId,
contractNo: this.form.contractNo,
contractName: this.form.contractName,
projectId,
});
}
} 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.clearReceiptAccount();
return;
}
Object.assign(this.form, {
contractNo: contract.contractNo || '',
contractName: contract.contractName || '',
payerName: contract.payerName || contract.partyA || '',
payeeName: contract.payeeName || contract.partyB || '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
});
await Promise.all([this.loadReceiptAccountOptions(), this.loadAttachmentRuleData([])]);
if (this.billPayment) this.loadBillOptions();
},
async loadReceiptAccountOptions(autoSelectDefault = true) {
const payeeName = String(this.form.payeeName || '').trim();
const preserveCurrentValue = autoSelectDefault === false;
this.receiptAccountOptions = [];
this.payeeCustomerLevel = '';
if (!payeeName) {
if (!preserveCurrentValue) this.clearReceiptAccount();
return;
}
this.receiptAccountLoading = true;
try {
const [fullNameResponse, shortNameResponse] = await Promise.all([
getCustomerArchiveList(1, 20, {
fullName: payeeName,
approvalStatus: 'approved',
status: 1,
}),
getCustomerArchiveList(1, 20, {
shortName: payeeName,
approvalStatus: 'approved',
status: 1,
}),
]);
const customers = [
...this.customerRecords(fullNameResponse),
...this.customerRecords(shortNameResponse),
];
const customer = customers.find(
item => item.fullName === payeeName || item.shortName === payeeName
);
if (!customer?.id) {
if (!preserveCurrentValue) this.clearReceiptAccount();
return;
}
const detail = this.unwrapData(await getCustomerArchiveDetail(customer.id));
this.payeeCustomerLevel = detail.customerLevel || '';
this.receiptAccountOptions = (detail.receiptAccounts || []).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;
}
},
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 || '',
bankName: account.bankName || '',
bankAccount: account.bankAccount || '',
});
this.$nextTick(() => this.$refs.formRef?.validateField('receiptAccountId').catch(() => {}));
},
handleReceiptAccountChange(id) {
const account = this.receiptAccountOptions.find(item => String(item.id) === String(id || ''));
if (account) {
this.applyReceiptAccount(account);
return;
}
this.clearReceiptAccount();
},
async handlePaymentMethodChange() {
if (!this.billPayment) {
this.form.billLedgerId = null;
this.form.billNo = '';
this.billOptions = [];
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;
}
},
handleBillChange(id) {
const selected = this.billOptions.find(item => String(item.id) === String(id));
this.form.billNo = selected?.billNo || '';
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() {
return Promise.all([this.loadFormalReferences(), this.loadPreReferences()]);
},
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.referenceVisible = true;
this.formalSelection = [];
this.preSelection = [];
this.formalPage.current = 1;
this.prePage.current = 1;
await this.loadSettlementReferences();
},
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: row.payeeName || row.partyB || '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: '',
});
this.referenceVisible = false;
await Promise.all([this.loadReceiptAccountOptions(), 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: row.payeeName,
});
this.referenceVisible = false;
this.refreshReferenceValidation();
const detail = this.unwrapData(detailResponse);
this.form.invoices = this.createInvoiceRows(
detail?.invoices,
detail?.formalSettlementNo || row.formalSettlementNo
);
this.createMockPaymentRecords(
row.formalSettlementNo,
this.referencePaymentAmount(settlementAmount)
);
await Promise.all([
this.loadReceiptAccountOptions(),
this.loadAttachmentRuleData(detail?.id ? [detail] : []),
]);
if (this.billPayment) this.loadBillOptions();
},
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)));
this.applyTransferredFormalSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
this.referenceVisible = false;
},
async selectPre(row) {
const validReferenceId = this.isValidReferenceId(row.id);
const [amountResponse, detailResponse] = await Promise.all([
validReferenceId
? api.getReferenceAmount('progress_advance', 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 })];
this.form.invoices = [];
this.paymentRecords = [];
this.clearReceiptAccount();
Object.assign(this.form, {
paymentType: 'progress_advance',
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: row.payeeName,
});
this.referenceVisible = false;
this.refreshReferenceValidation();
this.createMockPaymentRecords(
row.preSettlementNo,
this.referencePaymentAmount(settlementAmount)
);
const detail = this.unwrapData(detailResponse);
await Promise.all([
this.loadReceiptAccountOptions(),
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)));
this.applyTransferredPreSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
this.referenceVisible = false;
},
normalizeAttachments(files) {
this.form.attachments = (files || []).map(file => ({
...file,
attachmentType: (() => {
const matchedType = this.resolveAttachmentTypeByFileName(file);
const existingType = [
...Object.values(MATERIAL_TYPE_MAP),
'other',
].includes(file.attachmentType)
? file.attachmentType
: 'other';
return matchedType !== 'other' ? matchedType : existingType;
})(),
description: file.description || '',
}));
},
handleManualAttachmentUpload(uploadedFile) {
const uploadedUrl = this.attachmentFileUrl(uploadedFile);
const uploadedUid = uploadedFile?.uid;
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 = (this.form.attachments || []).map(file => {
const isUploadedFile =
(uploadedUid && file.uid === uploadedUid) ||
(uploadedUrl && this.attachmentFileUrl(file) === uploadedUrl);
return isUploadedFile
? {
...file,
uploadUserName,
uploadTime,
}
: file;
});
},
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(validateMaterials = false) {
await this.$refs.formRef.validate();
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.$message.warning('请选择所属项目');
return false;
}
if (validateMaterials && !this.validateRequiredAttachments()) 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;
this.$message.success('保存成功');
return true;
},
async submitForm() {
const saved = await this.saveDraft(true);
if (!saved) return;
await api.submit({ id: this.form.id });
this.$message.success('提交成功');
this.goBack();
},
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.push('/payment/payment-application');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.payment-form-page__form {
padding-bottom: 72px;
}
.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 {
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__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>