1、调整配载、总单

2、新增收付款模块
This commit is contained in:
2026-08-22 21:11:48 +08:00
parent 1962bee882
commit 5fcb82c0c0
51 changed files with 11663 additions and 1506 deletions
@@ -0,0 +1,852 @@
<template>
<basic-container class="payment-form-page">
<div class="payment-form-page__title">
{{ readonly ? '查看付款申请' : recordId ? '编辑付款申请' : '新增付款申请' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="payment-form-page__form"
>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">付款基本信息</div>
<el-row :gutter="24">
<el-col :span="12"
><el-form-item label="单据号"
><el-input
v-model="form.paymentNo"
disabled
placeholder="系统自动生成" /></el-form-item
></el-col>
<el-col :span="12"
><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="12"
><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="12"
><el-form-item label="所属项目"
><el-input v-model="form.projectName" disabled
><template v-if="form.paymentType === 'project_advance'" #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></el-form-item
></el-col
>
<el-col :span="12"
><el-form-item label="合同名称"
><el-input v-model="form.contractName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="结算金额"
><el-input v-model="form.settlementAmount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="可付款金额"
><el-input v-model="form.payableAmount" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="票款类型"
><el-input v-model="form.billType" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="付款比例" prop="paymentRatio"
><el-input-number
v-model="form.paymentRatio"
:disabled="readonly"
:min="0"
:max="100"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col :span="12"
><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="12"
><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="12"
><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="12"
><el-form-item label="收款方"
><el-input v-model="form.payeeName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="收款账号"
><el-input v-model="form.bankAccount" :disabled="readonly" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="开户银行"
><el-input v-model="form.bankName" :disabled="readonly" /></el-form-item
></el-col>
<el-col :span="12"
><el-form-item label="申请人"
><el-input v-model="form.applicantName" disabled /></el-form-item
></el-col>
<el-col :span="12"
><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>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">结算信息</div>
<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>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">发票信息</div>
<div class="payment-form-page__section-actions">
<el-button type="primary" plain :disabled="readonly" @click="addInvoice"
>新增发票</el-button
>
</div>
<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 label="发票号" min-width="140"
><template #default="{ row }"
><el-input v-model="row.invoiceNo" :disabled="readonly" /></template></el-table-column
><el-table-column label="开票日期" min-width="150"
><template #default="{ row }"
><el-date-picker
v-model="row.invoiceDate"
value-format="YYYY-MM-DD"
:disabled="readonly" /></template></el-table-column
><el-table-column label="发票类型" min-width="130"
><template #default="{ row }"
><el-input
v-model="row.invoiceType"
:disabled="readonly" /></template></el-table-column
><el-table-column label="税率" min-width="100"
><template #default="{ row }"
><el-input-number
v-model="row.taxRate"
:disabled="readonly"
:min="0"
:max="100"
:controls="false" /></template></el-table-column
><el-table-column label="发票金额(含税)" min-width="150"
><template #default="{ row }"
><el-input-number
v-model="row.invoiceAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false" /></template></el-table-column
><el-table-column label="匹配金额(含税)" min-width="150"
><template #default="{ row }"
><el-input-number
v-model="row.matchedAmount"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false" /></template></el-table-column
><el-table-column label="操作" width="100"
><template #default="{ $index }"
><el-link v-if="!readonly" type="danger" @click="form.invoices.splice($index, 1)"
>删除</el-link
></template
></el-table-column
></el-table
>
</section>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">付款记录</div>
<el-table :data="paymentRecords" border
><el-table-column type="index" label="序号" width="70" /><el-table-column
prop="paidAmount"
label="付款金额"
min-width="160" /><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>
<section class="payment-form-page__section">
<div class="payment-form-page__section-title">附件</div>
<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"
/><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="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 prop="originalName" label="文件名" min-width="220" /><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 prop="size" label="文件大小" width="120" /><el-table-column
prop="uploadUserName"
label="上传人"
width="130"
/><el-table-column prop="uploadTime" label="上传时间" width="170" /><el-table-column
label="操作"
width="100"
><template #default="{ row, $index }"
><el-link type="primary" :href="row.url || row.link" target="_blank">查看</el-link
><el-link v-if="!readonly" type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
></template
></el-table-column
></el-table
>
</section>
<div class="payment-form-page__actions">
<el-button @click="goBack">返回</el-button
><el-button v-if="!readonly && canSave" @click="saveDraft">保存</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
><el-tabs v-else v-model="referenceTab"
><el-tab-pane label="正式结算单" name="formal"
><el-table :data="formalRows" border @row-click="selectFormal"
><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></el-tab-pane
><el-tab-pane label="预结算单" name="pre"
><el-table :data="preRows" border @row-click="selectPre"
><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></el-tab-pane></el-tabs
></el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import * as billLedgerApi from '@/api/payment/billLedger';
import * as formalApi from '@/api/settlement/formalSettlement';
import * as preApi from '@/api/settlement/preSettlement';
const emptyForm = () => ({
id: null,
paymentNo: '',
paymentType: 'project_advance',
settlementId: null,
preSettlementId: null,
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',
data() {
return {
form: emptyForm(),
amountSyncing: false,
paymentRecords: [],
referenceVisible: false,
referenceTab: 'formal',
formalRows: [],
preRows: [],
contractRows: [],
billOptions: [],
billLoading: false,
paymentTypeOptions: api.paymentTypeOptions,
paymentMethodOptions: api.paymentMethodOptions,
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'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: '请选择付款方式' }],
billLedgerId: [{ validator: this.validateBillLedger, 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 ['bank_draft', 'commercial_draft'].includes(this.form.paymentMethod);
},
referenceLabel() {
return this.form.settlementNo || this.form.preSettlementNo || '';
},
settlementRows() {
if (!this.referenceLabel) return [];
return [
{
settlementNo: this.referenceLabel,
settlementAmount: this.form.settlementAmount,
payableAmount: this.form.payableAmount,
paymentRatio: this.form.paymentRatio,
appliedAmount: this.form.appliedAmount,
},
];
},
},
watch: {
'form.paymentRatio'(value) {
if (this.amountSyncing || !Number(this.form.payableAmount)) return;
this.amountSyncing = true;
this.form.appliedAmount = Number(
((Number(this.form.payableAmount) * Number(value || 0)) / 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
'form.appliedAmount'(value) {
if (this.amountSyncing || !Number(this.form.payableAmount)) return;
this.amountSyncing = true;
this.form.paymentRatio = Number(
((Number(value || 0) / Number(this.form.payableAmount)) * 100).toFixed(2)
);
this.$nextTick(() => {
this.amountSyncing = false;
});
},
},
created() {
this.initialize();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
validateReference(rule, value, callback) {
if (this.form.paymentType === 'progress_advance' && !this.form.preSettlementId) {
callback(new Error('请选择预结算单'));
return;
}
if (this.form.paymentType === 'settlement_payment' && !this.form.settlementId) {
callback(new Error('请选择正式结算单'));
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 (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() {
const appliedAmount = Number(this.form.appliedAmount || 0);
let matchedTotal = 0;
for (const invoice of this.form.invoices || []) {
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('单张发票匹配金额不能超过发票金额');
}
matchedTotal += matchedAmount;
}
if (matchedTotal > appliedAmount) {
throw new Error('发票匹配金额合计不能超过申请付款金额');
}
},
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,
attachments: this.parse(data.attachmentsJson),
invoices: data.invoices || [],
};
this.paymentRecords = data.paymentRecords || [];
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
} else {
this.form.applyDate = this.$dayjs().format('YYYY-MM-DD');
this.form.applicantName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
}
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
handleTypeChange() {
if (this.form.paymentType === 'project_advance') {
this.form.settlementId = null;
this.form.preSettlementId = null;
}
},
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(() => {});
},
async loadReferences() {
const [formalResponse, preResponse, contractResponse] = await Promise.all([
formalApi.getList(1, 100, { settlementType: 'payable', approvalStatus: 'approved' }),
preApi.getList(1, 100, { settlementType: 'payable', approvalStatus: 'approved' }),
formalApi.getContractOptions(''),
]);
const formal = this.unwrapData(formalResponse);
const pre = this.unwrapData(preResponse);
this.formalRows = formal.records || [];
this.preRows = pre.records || [];
this.contractRows = this.unwrapData(contractResponse) || [];
},
async openReference() {
await this.loadReferences();
this.referenceVisible = true;
},
selectContract(row) {
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,
payeeName: row.payeeName,
billType: '项目预付',
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
selectFormal(row) {
Object.assign(this.form, {
settlementId: row.id,
preSettlementId: null,
settlementNo: row.formalSettlementNo,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractName: row.contractName,
settlementAmount: row.settlementAmount,
payableAmount: Math.max(
0,
Number(row.settlementAmount || 0) - Number(row.appliedPaymentAmount || 0)
),
billType: '正式结算单',
payeeName: row.payeeName,
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
selectPre(row) {
Object.assign(this.form, {
preSettlementId: row.id,
settlementId: null,
preSettlementNo: row.preSettlementNo,
projectName: row.projectName,
deptId: row.deptId,
deptName: row.deptName,
contractName: row.contractName,
settlementAmount: row.settlementAmount,
payableAmount: Math.max(
0,
Number(row.settlementAmount || 0) - Number(row.advanceAppliedAmount || 0)
),
billType: '预结算单',
payeeName: row.payeeName,
});
this.referenceVisible = false;
if (this.billPayment) this.loadBillOptions();
},
addInvoice() {
this.form.invoices.push({
settlementNo: this.referenceLabel,
invoiceNo: '',
invoiceDate: '',
invoiceType: '',
taxRate: 3,
invoiceAmount: 0,
matchedAmount: 0,
attachmentJson: '',
});
},
normalizeAttachments(files) {
const userName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
attachmentType: file.attachmentType || 'other',
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
payload() {
const {
id,
paymentType,
settlementId,
preSettlementId,
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,
preSettlementId,
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,
};
},
async saveDraft() {
await this.$refs.formRef.validate();
if (this.form.paymentType === 'project_advance' && !this.form.projectId) {
this.$message.warning('请选择所属项目');
return false;
}
try {
this.validateInvoices();
} 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();
if (!saved) return;
await api.submit({ id: this.form.id });
this.$message.success('提交成功');
this.goBack();
},
goBack() {
this.$router.push('/payment/payment-application');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.payment-form-page__title,
.payment-form-page__section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.payment-form-page__title::before,
.payment-form-page__section-title::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
.payment-form-page__section {
margin-bottom: 24px;
}
.payment-form-page__section-title {
font-size: 16px;
}
.payment-form-page__section-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
}
.payment-form-page__actions {
display: flex;
justify-content: center;
gap: 16px;
padding: 24px 0;
border-top: 1px solid #eff1f7;
}
.payment-form-page :deep(.el-form-item) {
margin-bottom: 16px;
}
.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>