Files
tms-erp-web/src/views/payment/invoice-application-form.vue
T
gxwebsoft c9f673342a style(ui): 统一底部操作栏按钮间距,取消所有gap样式
- 移除各底部操作栏(footer/__actions/page-actions)内所有gap属性
- 按钮间距统一采用 Element Plus 全局默认的相邻按钮12px margin-left规则
- 修改 .el-dialog__footer 内.el-button间距由8px调整为12px,保持全站一致
- 确认 Avue 表单底部按钮无自带间距,去gap后间距由 Element Plus 提供
- 涉及模块覆盖 vehicle、payment、business、settlement,移除了相关组件样式中gap属性
- MEMORY.md规范更新,补充底栏按钮间距统一为无gap纯文字规则
2026-08-27 11:39:13 +08:00

1039 lines
36 KiB
Vue

<template>
<basic-container class="invoice-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="invoice-form-page__form"
>
<section-card title="开票基本信息">
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="结算单" prop="settlementIds">
<el-input :model-value="settlementLabel" readonly placeholder="请选择应付正式结算单">
<template v-if="!readonly" #append>
<el-button @click="openSettlementDialog">选择</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属项目">
<el-input v-model="form.projectName" disabled placeholder="根据结算单自动带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票单位">
<el-input v-model="form.issuerName" disabled placeholder="根据结算单自动带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="发票类型" prop="invoiceType">
<el-select
v-model="form.invoiceType"
:disabled="readonly"
@change="handleInvoiceTypeChange"
>
<el-option
v-for="item in invoiceTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="可开票金额">
<el-input
:model-value="form.settlements.length ? formatMoney(availableInvoiceAmount) : ''"
disabled
placeholder="根据结算单自动计算"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="本次开票金额">
<el-input
:model-value="invoiceAmount > 0 ? formatMoney(invoiceAmount) : ''"
disabled
placeholder="根据开票明细自动计算"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请日期">
<el-input v-model="form.applicationDate" 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.undertakingDeptName"
disabled
placeholder="根据结算单自动带出"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="部门邮箱" prop="departmentEmails">
<el-select
v-model="form.departmentEmails"
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
>
<el-option
v-for="email in departmentEmailOptions"
:key="email"
:label="email"
:value="email"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card title="受票方信息">
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="受票方单位" prop="receiverInvoiceInfoId">
<el-select
v-model="form.receiverInvoiceInfoId"
:disabled="readonly"
filterable
@change="handleReceiverChange"
>
<el-option
v-for="item in receiverInvoiceOptions"
:key="item.id"
:label="item.invoiceTitle"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="纳税人识别号" prop="taxpayerNo">
<el-input v-model="form.taxpayerNo" disabled placeholder="选择受票方单位后自动带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户账号" prop="bankAccount">
<el-input
v-model="form.bankAccount"
disabled
placeholder="选择受票方单位后自动带出"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户行" prop="bankName">
<el-input v-model="form.bankName" disabled placeholder="选择受票方单位后自动带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="注册地址" prop="registeredAddress">
<el-input
v-model="form.registeredAddress"
disabled
placeholder="选择受票方单位后自动带出"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系人">
<el-select
v-model="form.contactName"
:disabled="readonly"
clearable
filterable
allow-create
@change="handleContactChange"
>
<el-option
v-for="item in contactOptions"
:key="item.id"
:label="item.contactName"
:value="item.contactName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" :disabled="readonly" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" :disabled="readonly" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card title="开票内容"><template #extra><el-button v-if="!readonly" type="primary" @click="addSheet">添加</el-button></template>
<div v-for="(sheet, sheetIndex) in form.sheets" :key="sheet.localId" class="invoice-sheet">
<div class="invoice-sheet__header">
<span>第{{ sheetIndex + 1 }}张</span>
<el-link
v-if="!readonly && form.sheets.length > 1"
type="danger"
@click="removeSheet(sheetIndex)"
>删除</el-link
>
</div>
<el-table :data="sheet.lines" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="商品和服务分类" min-width="160">
<template #default="{ row }">
<el-select v-model="row.goodsCategory" :disabled="readonly">
<el-option
v-for="item in goodsCategoryOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="货物或服务简称" min-width="170">
<template #default="{ row }">
<el-select v-model="row.goodsName" :disabled="readonly" filterable allow-create>
<el-option
v-for="item in goodsNameOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="单位" min-width="100">
<template #default="{ row }">
<el-select v-model="row.unit" :disabled="readonly" filterable allow-create>
<el-option v-for="item in unitOptions" :key="item" :label="item" :value="item" />
</el-select>
</template>
</el-table-column>
<el-table-column label="数量" min-width="120">
<template #default="{ row }">
<el-input-number
v-model="row.quantity"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
<el-table-column label="不含税单价" min-width="130">
<template #default="{ row }">
<el-input-number
v-model="row.unitPriceNoTax"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
<el-table-column label="含税金额" min-width="130">
<template #default="{ row }">
<el-input-number
v-model="row.amountWithTax"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
@change="recalculateLine(row)"
/>
</template>
</el-table-column>
<el-table-column label="税率" min-width="110">
<template #default="{ row }">
<el-select
v-model="row.taxRate"
:disabled="readonly"
@change="recalculateLine(row)"
>
<el-option
v-for="item in taxRateOptions"
:key="item"
:label="`${item}%`"
:value="item"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="税额" min-width="120">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column label="备注" min-width="170">
<template #default="{ row }">
<el-input v-model="row.remark" :disabled="readonly" maxlength="200" />
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="110" fixed="right" align="center">
<template #default="{ $index }">
<div class="invoice-form-page__links">
<el-link type="primary" @click="addLine(sheet)">添加</el-link>
<el-link
v-if="sheet.lines.length > 1"
type="danger"
@click="sheet.lines.splice($index, 1)"
>删除</el-link
>
</div>
</template>
</el-table-column>
</el-table>
</div>
</section-card>
<section-card title="结算信息">
<el-table :data="form.settlements" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column label="结算总金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="可开票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="分摊发票金额" min-width="160">
<template #default="{ row }">
<el-input-number
v-model="row.allocatedInvoiceAmount"
:disabled="readonly"
:min="0"
:max="Number(row.availableInvoiceAmount || 0)"
:precision="2"
:controls="false"
/>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="开票明细">
<el-table
ref="detailTable"
:data="detailRows"
border
@selection-change="handleDetailSelection"
>
<el-table-column
v-if="!readonly"
type="selection"
width="52"
fixed="left"
align="center"
/>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column
v-for="column in detailColumns"
:key="column.prop"
v-bind="column"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link v-if="column.link && row[column.prop]" type="primary">
{{ row[column.prop] }}
</el-link>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="附件信息">
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="originalName" label="文件名" min-width="220" />
<el-table-column label="附件描述" min-width="240">
<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="140" />
<el-table-column prop="uploadTime" label="上传时间" width="170" />
<el-table-column label="操作" width="110" align="center">
<template #default="{ row, $index }">
<div class="invoice-form-page__links">
<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
>
</div>
</template>
</el-table-column>
</el-table>
<div class="invoice-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>
<section-card title="备注">
<el-form-item class="invoice-form-page__remark">
<el-input
v-model="form.remark"
:disabled="readonly"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
/>
</el-form-item>
</section-card>
<div class="invoice-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
<el-button
v-if="!readonly && canSave"
type="primary"
plain
@click="saveDraft"
>保存</el-button>
<el-button
v-if="!readonly && canSave && hasPermission('invoice_application_submit')"
type="primary"
@click="submitForm"
>提交</el-button>
</div>
</el-form>
<el-dialog
v-model="settlementDialog.visible"
title="选择应付正式结算单"
width="86%"
append-to-body
>
<div class="invoice-form-page__dialog-search">
<el-input
v-model="settlementDialog.keyword"
clearable
placeholder="结算单号项目或合同"
@keyup.enter="loadSettlementCandidates"
/>
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
</div>
<el-table
ref="settlementTable"
v-loading="settlementDialog.loading"
:data="settlementDialog.rows"
border
@selection-change="settlementDialog.selection = $event"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column prop="formalSettlementNo" label="结算单号" min-width="170" />
<el-table-column prop="projectName" label="所属项目" min-width="150" />
<el-table-column prop="deptName" label="所属组织" min-width="150" />
<el-table-column prop="contractNo" label="合同编号" min-width="150" />
<el-table-column prop="contractName" label="合同名称" min-width="170" />
<el-table-column prop="issuerName" label="开票方" min-width="150" />
<el-table-column prop="receiverName" label="受票方" min-width="150" />
<el-table-column label="结算金额" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column>
<el-table-column label="可开票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmSettlements">确定</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceApplication';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
let localId = 0;
const emptyLine = () => ({
localId: ++localId,
goodsCategory: '交通运输服务',
goodsName: '运费',
unit: '吨',
quantity: 0,
unitPriceNoTax: 0,
amountWithTax: 0,
taxRate: 0,
taxAmount: 0,
remark: '',
});
const emptySheet = () => ({ localId: ++localId, lines: [emptyLine()] });
const emptyForm = () => ({
id: null,
applicationNo: '',
projectName: '',
deptName: '',
issuerName: '',
invoiceType: 'electronic_special',
applicationDate: '',
applicantName: '',
undertakingDeptName: '',
departmentEmails: [],
receiverInvoiceInfoId: null,
receiverName: '',
taxpayerNo: '',
bankName: '',
bankAccount: '',
registeredAddress: '',
contactName: '',
contactPhone: '',
email: '',
settlements: [],
sheets: [emptySheet()],
attachments: [],
remark: '',
});
export default {
name: 'InvoiceApplicationForm',
data() {
return {
form: emptyForm(),
detailRows: [],
selectedDetailIds: [],
receiverInvoiceOptions: [],
contactOptions: [],
departmentEmailOptions: [],
invoiceTypeOptions: api.invoiceTypeOptions,
detailColumns: invoiceApplicationDetailColumns,
goodsCategoryOptions: ['交通运输服务', '生产生活服务', '其他现代服务'],
goodsNameOptions: ['运费', '装卸搬运服务', '过路费', '仓储服务', '代理服务费'],
unitOptions: ['吨', '车', '次', '项'],
taxRateOptions: [0, 1, 3, 6, 9, 13],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
'rar',
],
settlementDialog: {
visible: false,
loading: false,
keyword: '',
rows: [],
selection: [],
},
rules: {
settlementIds: [{ validator: this.validateSettlements, trigger: 'change' }],
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
departmentEmails: [
{ type: 'array', required: true, min: 1, message: '请选择部门邮箱', trigger: 'change' },
],
receiverInvoiceInfoId: [{ required: true, message: '请选择受票方单位', trigger: 'change' }],
contactPhone: [{ pattern: /^\d{11}$/, message: '联系电话必须为11位数字', trigger: 'blur' }],
taxpayerNo: [{ validator: this.validateInvoiceInfoField, trigger: 'change' }],
bankAccount: [{ validator: this.validateInvoiceInfoField, trigger: 'change' }],
bankName: [{ validator: this.validateInvoiceInfoField, trigger: 'change' }],
registeredAddress: [{ validator: this.validateInvoiceInfoField, trigger: 'change' }],
email: [{ validator: this.validateReceiverEmails, trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['permission']),
recordId() {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
},
canSave() {
const code = this.recordId ? 'invoice_application_edit' : 'invoice_application_add';
return this.hasPermission(code);
},
settlementLabel() {
return this.form.settlements
.map(item => item.formalSettlementNo)
.filter(Boolean)
.join(',');
},
settlementIds() {
return this.form.settlements.map(item => item.formalSettlementId || item.settlementId);
},
availableInvoiceAmount() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.availableInvoiceAmount || 0),
0
);
},
invoiceAmount() {
return this.form.sheets.reduce(
(sheetTotal, sheet) =>
sheetTotal +
sheet.lines.reduce((lineTotal, line) => lineTotal + Number(line.amountWithTax || 0), 0),
0
);
},
allocatedTotal() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.allocatedInvoiceAmount || 0),
0
);
},
},
created() {
this.initialize();
},
methods: {
hasPermission(code) {
return this.permission?.[code] !== false;
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
async initialize() {
const userInfo = this.$store.getters.userInfo || {};
const userEmail = userInfo.email || '';
this.departmentEmailOptions = userEmail ? [userEmail] : [];
if (!this.recordId) {
this.form.applicantName = userInfo.realName || userInfo.userName || '';
if (userEmail) this.form.departmentEmails = [userEmail];
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
departmentEmails: String(data.departmentEmails || '')
.split(';')
.filter(Boolean),
settlements: (data.settlements || []).map(item => ({
...item,
settlementId: item.formalSettlementId,
})),
sheets: (data.sheets || []).map(item => ({
...item,
localId: ++localId,
lines: (item.lines || []).map(line => ({ ...line, localId: ++localId })),
})),
attachments: this.parse(data.attachmentsJson),
};
if (!this.form.sheets.length) this.form.sheets = [emptySheet()];
this.selectedDetailIds = (data.details || []).map(item => item.formalSettlementDetailId);
if (this.settlementIds.length) {
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
}
this.$nextTick(this.restoreDetailSelection);
},
validateSettlements(rule, value, callback) {
if (!this.form.settlements.length) {
callback(new Error('请选择正式结算单'));
return;
}
callback();
},
validateInvoiceInfoField(rule, value, callback) {
const labels = {
taxpayerNo: '纳税人识别号',
bankAccount: '开户账号',
bankName: '开户行',
registeredAddress: '注册地址',
};
if (this.form.invoiceType === 'electronic_special' && !value) {
callback(new Error(`电子专票的${labels[rule.field]}不能为空`));
return;
}
callback();
},
validateReceiverEmails(rule, value, callback) {
if (!value) {
callback();
return;
}
const emails = String(value)
.split(/[;,,;]/)
.map(item => item.trim())
.filter(Boolean);
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emails.length > 3) {
callback(new Error('邮箱最多填写3个'));
return;
}
if (emails.some(email => !valid.test(email))) {
callback(new Error('请输入正确的邮箱,多个邮箱使用分号分隔'));
return;
}
callback();
},
handleInvoiceTypeChange() {
this.$nextTick(() => {
this.$refs.formRef?.validateField([
'taxpayerNo',
'bankAccount',
'bankName',
'registeredAddress',
]);
});
},
async openSettlementDialog() {
this.settlementDialog.visible = true;
await this.loadSettlementCandidates();
},
async loadSettlementCandidates() {
this.settlementDialog.loading = true;
try {
const data = this.unwrapData(
await api.getSettlementCandidates(this.settlementDialog.keyword)
);
this.settlementDialog.rows = Array.isArray(data) ? data : data?.records || [];
} finally {
this.settlementDialog.loading = false;
}
},
async confirmSettlements() {
const rows = this.settlementDialog.selection;
if (!rows.length) {
this.$message.warning('请至少选择一张应付正式结算单');
return;
}
const first = rows[0];
const incompatible = rows.some(
item =>
String(item.contractId) !== String(first.contractId) ||
String(item.projectId) !== String(first.projectId) ||
String(item.deptId) !== String(first.deptId) ||
item.issuerName !== first.issuerName ||
item.receiverName !== first.receiverName
);
if (incompatible) {
this.$message.warning('合并开票的结算单必须属于同一合同、项目、组织及收付款方');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
settlementId: item.id,
allocatedInvoiceAmount: 0,
}));
this.form.projectName = first.projectName;
this.form.deptName = first.deptName;
this.form.issuerName = first.issuerName;
this.form.undertakingDeptName = first.deptName;
this.settlementDialog.visible = false;
await Promise.all([this.loadSettlementDetails(), this.loadReceiverInformation()]);
},
async loadSettlementDetails(resetSelection = true) {
this.detailRows =
this.unwrapData(await api.getSettlementDetails(this.settlementIds.join(','))) || [];
if (resetSelection) this.selectedDetailIds = this.detailRows.map(item => item.id);
this.$nextTick(this.restoreDetailSelection);
},
async loadReceiverInformation() {
const data = this.unwrapData(await api.getReceiverInformation(this.settlementIds.join(',')));
this.receiverInvoiceOptions = data.invoiceInfos || [];
this.contactOptions = data.contacts || [];
this.form.issuerName = data.issuerName || this.form.issuerName;
if (!this.form.receiverInvoiceInfoId) {
const invoiceInfo =
this.receiverInvoiceOptions.find(item => item.isDefault === 1) ||
this.receiverInvoiceOptions[0];
if (invoiceInfo) {
this.form.receiverInvoiceInfoId = invoiceInfo.id;
this.handleReceiverChange(invoiceInfo.id);
}
}
if (!this.form.contactName) {
const contact =
this.contactOptions.find(item => item.isDefault === 1) || this.contactOptions[0];
if (contact) {
this.form.contactName = contact.contactName;
this.handleContactChange(contact.contactName);
}
}
},
handleReceiverChange(id) {
const info = this.receiverInvoiceOptions.find(item => String(item.id) === String(id));
if (!info) return;
Object.assign(this.form, {
receiverName: info.invoiceTitle,
taxpayerNo: info.taxNo,
bankName: info.bankName,
bankAccount: info.bankAccount,
registeredAddress: info.registeredAddress,
email: info.email || '',
});
this.handleInvoiceTypeChange();
},
handleContactChange(name) {
const contact = this.contactOptions.find(item => item.contactName === name);
if (!contact) return;
this.form.contactPhone = contact.contactPhone || '';
this.form.email = contact.email || this.form.email || '';
},
handleDetailSelection(rows) {
if (this.readonly) return;
this.selectedDetailIds = rows.map(item => item.formalSettlementDetailId || item.id);
},
restoreDetailSelection() {
if (this.readonly || !this.$refs.detailTable) return;
const selected = new Set(this.selectedDetailIds.map(String));
this.detailRows.forEach(row => {
const id = row.formalSettlementDetailId || row.id;
this.$refs.detailTable.toggleRowSelection(row, selected.has(String(id)));
});
},
addSheet() {
this.form.sheets.push(emptySheet());
},
removeSheet(index) {
this.form.sheets.splice(index, 1);
},
addLine(sheet) {
sheet.lines.push(emptyLine());
},
recalculateLine(row) {
const amount = Number(row.amountWithTax || 0);
const rate = Number(row.taxRate || 0) / 100;
row.taxAmount = rate ? Number((amount - amount / (1 + rate)).toFixed(2)) : 0;
},
normalizeAttachments(files) {
const userInfo = this.$store.getters.userInfo || {};
const userName = userInfo.realName || userInfo.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.form.attachments = (files || []).map(file => ({
...file,
description: file.description || '',
uploadUserName: file.uploadUserName || userName,
uploadTime: file.uploadTime || time,
}));
},
validateBusiness() {
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
if (this.invoiceAmount <= 0) throw new Error('本次开票金额必须大于0');
if (Math.abs(this.invoiceAmount - this.allocatedTotal) > 0.001) {
throw new Error('结算单分摊金额合计必须等于本次开票金额');
}
for (const sheet of this.form.sheets) {
if (!sheet.lines.length) throw new Error('每张发票至少需要一条商品行');
const hasFreight = sheet.lines.some(line => line.goodsName === '运费');
const hasOther = sheet.lines.some(line => line.goodsName !== '运费');
if (hasFreight && hasOther) throw new Error('运费不能与其他费用合并开在同一张发票中');
for (const line of sheet.lines) {
if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息');
if (
[line.quantity, line.unitPriceNoTax, line.amountWithTax, line.taxRate].some(
Number.isNaN
)
) {
throw new Error('开票商品行金额格式不正确');
}
}
}
},
payload() {
return {
id: this.form.id,
invoiceType: this.form.invoiceType,
departmentEmails: this.form.departmentEmails.join(';'),
receiverInvoiceInfoId: this.form.receiverInvoiceInfoId,
contactName: this.form.contactName,
contactPhone: this.form.contactPhone,
email: this.form.email,
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
settlements: this.form.settlements.map(item => ({
settlementId: item.formalSettlementId || item.settlementId,
allocatedInvoiceAmount: item.allocatedInvoiceAmount,
})),
detailIds: this.selectedDetailIds,
sheets: this.form.sheets.map(sheet => ({
lines: sheet.lines.map(line => ({
goodsCategory: line.goodsCategory,
goodsName: line.goodsName,
unit: line.unit,
quantity: line.quantity,
unitPriceNoTax: line.unitPriceNoTax,
amountWithTax: line.amountWithTax,
taxRate: line.taxRate,
taxAmount: line.taxAmount,
remark: line.remark,
})),
})),
};
},
async saveDraft() {
await this.$refs.formRef.validate();
try {
this.validateBusiness();
} catch (error) {
this.$message.warning(error.message);
return false;
}
const id = this.unwrapData(await api.save(this.payload()));
this.form.id = id;
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();
},
async syncReferenceData() {
if (!this.settlementIds.length) {
this.$message.warning('请先选择正式结算单');
return;
}
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
this.$message.success('关联信息已同步');
},
goBack() {
this.$router.push('/payment/invoice-application');
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.invoice-form-page__form :deep(.el-select),
.invoice-form-page__form :deep(.el-input-number) {
width: 100%;
}
.invoice-sheet {
margin-top: 16px;
}
.invoice-sheet__header {
min-height: 42px;
padding: 0 12px;
border: 1px solid #eff1f7;
border-bottom: 0;
background: #fafafa;
font-weight: 600;
}
.invoice-form-page__links {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.invoice-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.invoice-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 .invoice-form-page__actions) {
left: 60px;
}
:global(.avue-layout--horizontal .invoice-form-page__actions) {
left: 0;
}
.invoice-form-page__attachment-upload {
display: flex;
justify-content: flex-start;
margin-top: 12px;
}
.invoice-form-page__attachment-upload .vehicle-attachment-upload {
width: auto;
}
.invoice-form-page__attachment-upload .el-upload {
display: inline-flex;
}
.invoice-form-page__dialog-search {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.invoice-form-page__dialog-search .el-input {
width: 360px;
}
.invoice-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.invoice-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 1200px) {
.invoice-form-page__form :deep(.el-col-6) {
max-width: 50%;
flex: 0 0 50%;
}
}
@media (max-width: 768px) {
.invoice-form-page__form :deep(.el-col-6) {
max-width: 100%;
flex: 0 0 100%;
}
}
</style>