Files
tms-erp-web/src/views/payment/bill-ledger-form.vue
T
2026-08-28 15:30:24 +08:00

594 lines
20 KiB
Vue

<template>
<basic-container class="bill-ledger-form-page">
<div class="archive-page-form__title">
{{ recordId ? '编辑汇票台账' : '新增汇票台账' }}
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="auto"
class="bill-ledger-form-page__form"
>
<section-card title="基本信息">
<el-row :gutter="24" class="bill-ledger-form-page__basic-grid">
<el-col :span="6">
<el-form-item label="票据号码" prop="billNo">
<el-input
v-model="form.billNo"
:disabled="Boolean(recordId)"
maxlength="32"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="汇票类型" prop="billType">
<el-select v-model="form.billType" placeholder="请选择">
<el-option
v-for="item in billTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出票单位" prop="issuerId">
<el-select
v-model="form.issuerId"
filterable
remote
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
>
<el-option
v-for="item in customerOptions"
:key="item.id"
:label="customerName(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="收票单位" prop="receiverName">
<el-select
v-model="form.receiverName"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
>
<el-option
v-for="item in receiverOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="票面金额" prop="faceAmount">
<el-input-number
v-model="form.faceAmount"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="可用余额">
<el-input :model-value="formatMoney(projectedAvailableBalance)" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出票日期" prop="issueDate">
<el-date-picker
v-model="form.issueDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="到期日期" prop="maturityDate">
<el-date-picker
v-model="form.maturityDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="可用部门" prop="availableDeptIds">
<el-tree-select
v-model="form.availableDeptIds"
:data="deptTree"
:props="deptProps"
node-key="id"
multiple
show-checkbox
collapse-tags
collapse-tags-tooltip
placeholder="请选择,默认全部"
@change="handleDeptChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出票行" prop="issuingBank">
<el-select
v-model="form.issuingBank"
filterable
allow-create
default-first-option
placeholder="请选择或输入"
>
<el-option v-for="item in bankOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="费用承担方" prop="feeBearerId">
<el-select
v-model="form.feeBearerId"
filterable
remote
reserve-keyword
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="请选择客商基础档案"
>
<el-option
v-for="item in customerOptions"
:key="item.id"
:label="customerName(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="银行贴现参考率" prop="bankDiscountReferenceRate">
<el-input-number
v-model="form.bankDiscountReferenceRate"
:min="0"
:max="100"
:precision="4"
:controls="false"
placeholder="请输入"
>
<template #suffix>%</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="双方确认贴现率" prop="confirmedDiscountRate">
<el-input-number
v-model="form.confirmedDiscountRate"
:min="0"
:max="100"
:precision="4"
:controls="false"
placeholder="请输入"
>
<template #suffix>%</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="预计贴现费用">
<el-input :model-value="formatMoney(estimatedDiscountFee)" disabled />
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" />
</section-card>
<section-card title="备注">
<el-form-item prop="remark" class="bill-ledger-form-page__remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</section-card>
<section-card v-if="readonly" title="使用记录">
<el-table :data="usageRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="applicationNo" label="申请单号" min-width="180" align="center" />
<el-table-column label="使用金额" min-width="150" align="center">
<template #default="{ row }">{{ formatMoney(row.usedAmount) }}</template>
</el-table-column>
<el-table-column prop="useDeptName" label="使用部门" min-width="180" align="center" />
<el-table-column label="状态" min-width="120" align="center">
<template #default>已审核</template>
</el-table-column>
</el-table>
</section-card>
<div class="bill-ledger-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button type="primary" :loading="saving" @click="confirmSubmit">确认</el-button>
</div>
</el-form>
</basic-container>
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/billLedger';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getDeptTree } from '@/api/system/dept';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const emptyForm = () => ({
id: null,
billNo: '',
issuerId: null,
receiverName: '',
billType: 'issued',
faceAmount: null,
availableBalance: 0,
issueDate: '',
maturityDate: '',
availableDeptIds: ['all'],
availableDeptNames: '全部',
feeBearerId: null,
confirmedDiscountRate: null,
issuingBank: '',
bankDiscountReferenceRate: null,
attachments: [],
remark: '',
});
export default {
name: 'BillLedgerForm',
components: { VehicleAttachmentTable },
data() {
return {
form: emptyForm(),
usageRecords: [],
billTypeOptions: api.billTypeOptions,
customerOptions: [],
receiverOptions: [],
customerLoading: false,
saving: false,
deptTree: [],
deptMap: new Map(),
deptProps: { label: 'title', value: 'id', children: 'children' },
bankOptions: [
'中国工商银行',
'中国农业银行',
'中国银行',
'中国建设银行',
'交通银行',
'招商银行',
'浦发银行',
'中信银行',
],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
rules: {
billNo: [
{ required: true, message: '请输入票据号码', trigger: 'blur' },
{ max: 32, message: '票据号码不能超过32个字符', trigger: 'blur' },
],
issuerId: [{ required: true, message: '请选择出票单位', trigger: 'change' }],
receiverName: [{ required: true, message: '请选择或输入收票单位', trigger: 'change' }],
billType: [{ required: true, message: '请选择汇票类型', trigger: 'change' }],
faceAmount: [{ validator: this.validateFaceAmount, trigger: 'change' }],
issueDate: [{ required: true, message: '请选择出票日期', trigger: 'change' }],
maturityDate: [{ required: true, validator: this.validateMaturityDate, trigger: 'change' }],
availableDeptIds: [{ validator: this.validateDepartments, trigger: 'change' }],
feeBearerId: [{ required: true, message: '请选择费用承担方', trigger: 'change' }],
confirmedDiscountRate: [{ validator: this.validateRate, trigger: 'change' }],
issuingBank: [
{ required: true, message: '请选择或输入出票行', trigger: 'change' },
{ max: 100, message: '出票行不能超过100个字符', trigger: 'change' },
],
bankDiscountReferenceRate: [{ validator: this.validateRate, trigger: 'change' }],
remark: [{ max: 200, message: '备注不能超过200个字符', trigger: 'blur' }],
},
};
},
computed: {
...mapGetters(['userInfo']),
recordId() {
return this.$route.query.id || '';
},
activeUsedAmount() {
return this.usageRecords.reduce((total, item) => total + Number(item.usedAmount || 0), 0);
},
projectedAvailableBalance() {
return Math.max(Number(this.form.faceAmount || 0) - this.activeUsedAmount, 0);
},
estimatedDiscountFee() {
return (
(Number(this.form.faceAmount || 0) * Number(this.form.confirmedDiscountRate || 0)) / 100
);
},
},
created() {
this.initialize();
},
methods: {
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async initialize() {
await Promise.all([this.loadDepartments(), this.searchCustomers('')]);
if (!this.recordId) {
this.form.issueDate = this.$dayjs().format('YYYY-MM-DD');
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
...emptyForm(),
...data,
confirmedDiscountRate: this.normalizeDiscountRate(data.confirmedDiscountRate),
bankDiscountReferenceRate: this.normalizeDiscountRate(data.bankDiscountReferenceRate),
availableDeptIds: this.parse(data.availableDeptIdsJson),
attachments: this.parse(data.attachmentsJson),
};
this.usageRecords = data.usageRecords || [];
this.receiverOptions = data.receiverName ? [data.receiverName] : [];
this.ensureCustomerOption(data.issuerId, data.issuerName);
this.ensureCustomerOption(data.feeBearerId, data.feeBearerName);
this.handleDeptChange(this.form.availableDeptIds);
},
parse(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
return JSON.parse(value) || [];
} catch {
return [];
}
},
async loadDepartments() {
const response = await getDeptTree(this.userInfo?.tenantId);
const rows = this.unwrapData(response) || [];
this.deptTree = [{ id: 'all', title: '全部', children: rows }];
this.deptMap = new Map([['all', '全部']]);
this.flattenDepartments(rows).forEach(item => this.deptMap.set(String(item.id), item.title));
},
flattenDepartments(rows) {
return (rows || []).flatMap(item => [item, ...this.flattenDepartments(item.children || [])]);
},
handleDeptChange(values) {
const ids = values || [];
this.form.availableDeptNames = ids.some(item => String(item) === 'all')
? '全部'
: ids
.map(item => this.deptMap.get(String(item)))
.filter(Boolean)
.join('、');
},
async searchCustomers(keyword) {
this.customerLoading = true;
try {
const data = this.unwrapData(
await getCustomerList(1, 100, { approvalStatus: 'approved', fullName: keyword || '' })
);
const current = this.customerOptions.filter(item =>
[this.form.issuerId, this.form.feeBearerId].some(id => String(id) === String(item.id))
);
this.customerOptions = [...current, ...(data.records || [])].filter(
(item, index, rows) => rows.findIndex(row => String(row.id) === String(item.id)) === index
);
this.receiverOptions = [
...new Set(this.customerOptions.map(item => this.customerName(item)).filter(Boolean)),
];
} finally {
this.customerLoading = false;
}
},
ensureCustomerOption(id, name) {
if (!id || this.customerOptions.some(item => String(item.id) === String(id))) return;
this.customerOptions.push({ id, fullName: name, shortName: name });
},
customerName(item) {
return item.fullName || item.shortName || item.customerCode || '-';
},
validateFaceAmount(rule, value, callback) {
if (!Number.isFinite(Number(value)) || Number(value) <= 0) {
callback(new Error('票面金额必须大于0'));
return;
}
if (Number(value) < this.activeUsedAmount) {
callback(new Error('票面金额不能小于已使用金额'));
return;
}
callback();
},
validateMaturityDate(rule, value, callback) {
if (!value) {
callback(new Error('请选择到期日期'));
return;
}
if (this.form.issueDate && !this.$dayjs(value).isAfter(this.$dayjs(this.form.issueDate))) {
callback(new Error('到期日期必须晚于出票日期'));
return;
}
callback();
},
validateDepartments(rule, value, callback) {
if (!Array.isArray(value) || !value.length) {
callback(new Error('请选择可用部门'));
return;
}
callback();
},
validateRate(rule, value, callback) {
if (value !== null && value !== undefined && value !== '') {
const rate = Number(value);
if (!Number.isFinite(rate) || rate < 0 || rate > 100) {
callback(new Error('贴现率必须在0-100之间'));
return;
}
}
callback();
},
normalizeDiscountRate(value) {
return value === null || value === undefined || value === '' || Number(value) === -1
? null
: value;
},
normalizeAttachments(files) {
const userName = this.userInfo?.realName || this.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,
}));
},
fileSize(file) {
if (typeof file.size === 'string' && /[a-z]/i.test(file.size)) return file.size;
const bytes = Number(file.size || file.fileSize || 0);
if (!bytes) return '-';
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
},
payload() {
return {
id: this.form.id,
billNo: this.form.billNo,
issuerId: this.form.issuerId,
receiverName: this.form.receiverName,
billType: this.form.billType,
faceAmount: this.form.faceAmount,
issueDate: this.form.issueDate,
maturityDate: this.form.maturityDate,
availableDeptIdsJson: JSON.stringify(this.form.availableDeptIds || []),
availableDeptNames: this.form.availableDeptNames,
feeBearerId: this.form.feeBearerId,
confirmedDiscountRate: this.normalizeDiscountRate(this.form.confirmedDiscountRate),
issuingBank: this.form.issuingBank,
bankDiscountReferenceRate: this.normalizeDiscountRate(this.form.bankDiscountReferenceRate),
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
};
},
async confirmSubmit() {
await this.$refs.formRef.validate();
this.saving = true;
try {
await api.submit(this.payload());
this.$message.success(this.recordId ? '编辑成功' : '新增成功');
this.goBack();
} finally {
this.saving = false;
}
},
goBack() {
this.$router.push('/payment/bill-ledger');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped lang="scss">
.bill-ledger-form-page__form :deep(.el-form-item) {
margin-bottom: 14px;
}
.bill-ledger-form-page__basic-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-flow: row;
}
.bill-ledger-form-page__basic-grid > .el-col {
width: 100%;
max-width: 100%;
flex: none;
}
.bill-ledger-form-page__form :deep(.el-input),
.bill-ledger-form-page__form :deep(.el-select),
.bill-ledger-form-page__form :deep(.el-input-number),
.bill-ledger-form-page__form :deep(.el-date-editor),
.bill-ledger-form-page__form :deep(.el-tree-select) {
width: 100%;
}
.bill-ledger-form-page__form :deep(.el-input-number .el-input__inner) {
text-align: left;
}
.bill-ledger-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.bill-ledger-form-page__uploader {
margin-top: 12px;
}
.bill-ledger-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 .bill-ledger-form-page__actions) {
left: 60px;
}
:global(.avue-layout--horizontal .bill-ledger-form-page__actions) {
left: 0;
}
.bill-ledger-form-page :deep(.el-table) {
--el-table-border-color: #eff1f7;
}
.bill-ledger-form-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
@media (max-width: 1024px) {
.bill-ledger-form-page__basic-grid {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: none;
grid-auto-flow: row;
}
}
</style>