完善收付款管理

This commit is contained in:
2026-08-27 23:52:17 +08:00
parent e98aa61cd2
commit b446e34c94
14 changed files with 783 additions and 629 deletions
+31
View File
@@ -0,0 +1,31 @@
import request from '@/axios';
export const getList = (current, size, params) =>
request({
url: '/blade-system/invoice-item/list',
method: 'get',
params: { ...params, current, size },
});
export const getAll = () => getList(1, 1000, {});
export const getDetail = id =>
request({
url: '/blade-system/invoice-item/detail',
method: 'get',
params: { id },
});
export const submit = row =>
request({
url: '/blade-system/invoice-item/submit',
method: 'post',
data: row,
});
export const remove = ids =>
request({
url: '/blade-system/invoice-item/remove',
method: 'post',
params: { ids },
});
+1 -1
View File
@@ -12,7 +12,7 @@ export const getSettlementCandidates = (keyword, flowId) =>
params: { keyword, flowId },
});
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
export const sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data });
export const claimStatusOptions = [
{ label: '未认领', value: 'unclaimed' },
-1
View File
@@ -30,5 +30,4 @@ export const invoiceApplicationDetailColumns = [
{ prop: 'mileage', label: '里程(KM', minWidth: 120 },
{ prop: 'batchNo', label: '批次号', minWidth: 120 },
{ prop: 'freightAmount', label: '运费', minWidth: 120, money: true },
{ prop: 'feeItemsJson', label: '费用项目', minWidth: 180 },
];
+308
View File
@@ -0,0 +1,308 @@
<template>
<basic-container class="invoice-item-page">
<avue-crud
ref="crud"
v-model="form"
v-model:page="page"
:option="option"
:data="data"
:table-loading="loading"
:permission="permissionList"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu="scope">
<el-link
v-if="permission.invoice_item_edit"
type="primary"
@click="$refs.crud.rowEdit(scope.row, scope.index)"
>
修改
</el-link>
<el-link
v-if="permission.invoice_item_delete"
type="danger"
@click="rowDel(scope.row)"
>
删除
</el-link>
</template>
<template #defaultTaxRate="scope">{{ scope.row.defaultTaxRate }}%</template>
<template #defaultTaxRate-form>
<el-input
v-model="form.defaultTaxRate"
inputmode="decimal"
maxlength="6"
clearable
placeholder="请输入默认税率"
@input="handleTaxRateInput"
>
<template #append>%</template>
</el-input>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import { getList, getDetail, submit, remove } from '@/api/base/invoice-item';
import { mapGetters } from 'vuex';
export default {
data() {
return {
form: {},
data: [],
loading: true,
query: {},
selectionList: [],
page: {
pageSize: 10,
pageSizes: [10, 30, 50, 100, 200],
currentPage: 1,
total: 0,
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
border: true,
stripe: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
selection: false,
addBtn: true,
editBtn: false,
delBtn: false,
viewBtn: false,
menuWidth: 140,
dialogWidth: 900,
dialogClickModal: false,
labelPosition: 'right',
labelWidth: 'auto',
searchShow: true,
searchIcon: true,
searchIndex: 4,
searchMenuSpan: 24,
searchMenuPosition: 'right',
column: [
{
label: '货物或服务简称',
prop: 'shortName',
minWidth: 160,
span: 12,
maxlength: 100,
rules: [{ required: true, message: '请输入货物或服务简称', trigger: 'blur' }],
},
{
label: '税收分类编码',
prop: 'taxClassificationCode',
minWidth: 190,
span: 12,
maxlength: 30,
rules: [{ required: true, message: '请输入税收分类编码', trigger: 'blur' }],
},
{
label: '商品和服务分类名称',
prop: 'categoryName',
minWidth: 220,
span: 12,
maxlength: 200,
rules: [{ required: true, message: '请输入商品和服务分类名称', trigger: 'blur' }],
},
{
label: '默认税率',
prop: 'defaultTaxRate',
type: 'input',
formslot: true,
slot: true,
minWidth: 120,
span: 12,
rules: [
{ required: true, message: '请输入默认税率', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const text = String(value ?? '');
const rate = Number(text);
if (!/^\d+(\.\d+)?$/.test(text) || rate < 0 || rate > 100) {
callback(new Error('默认税率必须是0到100之间的数字'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '税收项目',
prop: 'taxProject',
search: true,
searchOrder: 2,
hide: true,
display: false,
searchPlaceholder: '请输入税收项目',
},
{
label: '税收项目简称',
prop: 'taxProjectShortName',
search: true,
searchOrder: 1,
hide: true,
display: false,
searchPlaceholder: '请输入税收项目简称',
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
viewDisplay: false,
display: false,
},
{
label: '更新人',
prop: 'updateUserName',
addDisplay: false,
editDisplay: false,
viewDisplay: false,
display: false,
},
],
},
};
},
computed: {
...mapGetters(['permission']),
permissionList() {
return {
addBtn: this.validData(this.permission.invoice_item_add, false),
editBtn: false,
delBtn: false,
};
},
},
methods: {
rowSave(row, done, loading) {
submit(this.normalizeTaxRateRow(row)).then(
() => {
this.onLoad(this.page);
this.$message.success('操作成功');
done();
},
() => loading()
);
},
rowUpdate(row, index, done, loading) {
submit(this.normalizeTaxRateRow(row)).then(
() => {
this.onLoad(this.page);
this.$message.success('操作成功');
done();
},
() => loading()
);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', '提示', { type: 'warning' })
.then(() => remove(row.id))
.then(() => {
this.onLoad(this.page);
this.$message.success('操作成功');
});
},
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
done();
});
return;
}
this.form = { defaultTaxRate: 0 };
done();
},
handleTaxRateInput(value) {
let normalized = String(value || '').replace(/[^\d.]/g, '');
normalized = normalized.replace(/(\..*)\./g, '$1');
if (normalized.startsWith('.')) normalized = `0${normalized}`;
this.form.defaultTaxRate = normalized;
},
normalizeTaxRateRow(row) {
const values = { ...row };
values.defaultTaxRate = Number(values.defaultTaxRate);
return values;
},
searchChange(params, done) {
this.query = {
...params,
categoryName: params.taxProject,
shortName: params.taxProjectShortName,
};
delete this.query.taxProject;
delete this.query.taxProjectShortName;
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
searchReset() {
this.query = {};
this.page.currentPage = 1;
this.onLoad(this.page);
},
selectionChange(list) {
this.selectionList = list;
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page);
},
onLoad(page) {
this.loading = true;
getList(page.currentPage, page.pageSize, this.query).then(res => {
const result = res.data.data || {};
this.data = result.records || [];
this.page.total = result.total || 0;
this.loading = false;
});
},
},
};
</script>
<style scoped>
.invoice-item-page :deep(.avue-crud__menu) {
margin-bottom: 8px;
}
.invoice-item-page :deep(.avue-crud__dialog .el-form-item) {
margin-bottom: 28px;
}
.invoice-item-page :deep(.avue-crud__dialog .el-form-item__error) {
position: relative;
line-height: 20px;
white-space: normal;
}
.invoice-item-page :deep(.avue-crud__menu .el-link) {
margin-right: 8px;
}
</style>
+3 -42
View File
@@ -195,48 +195,7 @@
</section-card>
<section-card title="附件信息">
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">
<el-link
v-if="row.url || row.link"
type="primary"
:href="row.url || row.link"
target="_blank"
>
{{ row.originalName || row.name || '附件' }}
</el-link>
<span v-else>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240">
<template #default="{ row }">
<el-input v-model="row.description" maxlength="200" placeholder="请输入" />
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ fileSize(row) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="130" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ $index }">
<el-link type="danger" @click="form.attachments.splice($index, 1)">删除</el-link>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="form.attachments"
class="bill-ledger-form-page__uploader"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传"
@change="normalizeAttachments"
/>
<vehicle-attachment-table v-model="form.attachments" />
</section-card>
<section-card title="备注">
@@ -279,6 +238,7 @@ 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,
@@ -302,6 +262,7 @@ const emptyForm = () => ({
export default {
name: 'BillLedgerForm',
components: { VehicleAttachmentTable },
data() {
return {
form: emptyForm(),
+8 -45
View File
@@ -75,51 +75,7 @@
</section-card>
<section-card title="附件信息">
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">
<el-link
v-if="row.url || row.link"
type="primary"
:href="row.url || row.link"
target="_blank"
>
{{ row.originalName || row.name || '附件' }}
</el-link>
<span v-else>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<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 label="文件大小" width="120" align="center">
<template #default="{ row }">{{ fileSize(row) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="130" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" />
<el-table-column label="操作" width="100" align="center">
<template #default="{ $index }">
<el-link v-if="!readonly" type="danger" @click="form.attachments.splice($index, 1)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-if="!readonly"
v-model="form.attachments"
class="bill-payment-form-page__uploader"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传"
@change="normalizeAttachments"
/>
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card>
<div class="bill-payment-form-page__actions">
@@ -163,6 +119,11 @@
<template #default="{ row }">{{ formatMoney(row.availableBalance) }}</template>
</el-table-column>
<el-table-column prop="maturityDate" label="到期日期" min-width="120" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectBill(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="billDialog.visible = false">取消</el-button>
@@ -177,6 +138,7 @@ import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/billPayment';
import * as billLedgerApi from '@/api/payment/billLedger';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const emptyForm = () => ({
id: null,
@@ -194,6 +156,7 @@ const emptyForm = () => ({
export default {
name: 'BillPaymentForm',
components: { VehicleAttachmentTable },
data() {
return {
Search,
@@ -1,7 +1,6 @@
<template>
<section class="bill-ledger-table">
<div class="bill-ledger-table__toolbar">
<div class="bill-ledger-table__title">汇票台账列表</div>
<el-button v-if="hasPermission('bill_ledger_add')" type="primary" @click="$emit('add')">
新增
</el-button>
@@ -94,13 +93,9 @@ export default {
.bill-ledger-table__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
padding: 12px 0;
}
.bill-ledger-table__title {
font-size: 16px;
font-weight: 600;
}
.bill-ledger-table__links {
display: flex;
flex-wrap: wrap;
+229 -119
View File
@@ -93,9 +93,9 @@
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
placeholder="请选择客商开票信息邮箱"
no-data-text="当前客商开票信息未配置邮箱"
>
<el-option
v-for="email in departmentEmailOptions"
@@ -203,9 +203,14 @@
<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-select
v-model="row.goodsCategory"
:disabled="readonly"
filterable
@change="handleGoodsCategoryChange(row)"
>
<el-option
v-for="item in goodsCategoryOptions"
v-for="item in invoiceItemCategories"
:key="item"
:label="item"
:value="item"
@@ -215,12 +220,17 @@
</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-select
v-model="row.goodsName"
:disabled="readonly"
filterable
@change="handleGoodsNameChange(row)"
>
<el-option
v-for="item in goodsNameOptions"
:key="item"
:label="item"
:value="item"
v-for="item in invoiceItemNames(row.goodsCategory)"
:key="item.shortName"
:label="item.shortName"
:value="item.shortName"
/>
</el-select>
</template>
@@ -358,7 +368,15 @@
<el-link v-if="column.link && row[column.prop]" type="primary">
{{ row[column.prop] }}
</el-link>
<span
v-else-if="
['transportQuantity', 'mileage'].includes(column.prop) &&
Number(row[column.prop]) === -1
"
></span>
<span v-else-if="column.feeItemKeys">{{ formatMoney(getFeeItemValue(row, column.feeItemKeys)) }}</span>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else-if="column.prop === 'transportType'">{{ transportTypeLabel(row.transportType) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -366,41 +384,7 @@
</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>
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card>
<section-card title="备注">
@@ -469,6 +453,11 @@
<el-table-column label="可开票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectSettlement(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
@@ -481,13 +470,16 @@
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceApplication';
import { getAll as getInvoiceItems } from '@/api/base/invoice-item';
import { getDictionary } from '@/api/system/dictbiz';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
let localId = 0;
const emptyLine = () => ({
localId: ++localId,
goodsCategory: '交通运输服务',
goodsName: '运费',
goodsCategory: '',
goodsName: '',
unit: '吨',
quantity: 0,
unitPriceNoTax: 0,
@@ -525,6 +517,9 @@ const emptyForm = () => ({
export default {
name: 'InvoiceApplicationForm',
components: {
VehicleAttachmentTable,
},
data() {
return {
form: emptyForm(),
@@ -533,29 +528,14 @@ export default {
receiverInvoiceOptions: [],
contactOptions: [],
departmentEmailOptions: [],
invoiceItems: [],
transportTypeOptions: [],
invoiceTypeOptions: api.invoiceTypeOptions,
detailColumns: invoiceApplicationDetailColumns,
goodsCategoryOptions: ['交通运输服务', '生产生活服务', '其他现代服务'],
goodsNameOptions: ['运费', '装卸搬运服务', '过路费', '仓储服务', '代理服务费'],
detailBaseColumns: invoiceApplicationDetailColumns.filter(
column => !['feeItemsJson', 'freightAmount'].includes(column.prop)
),
feeItemColumns: [],
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,
@@ -566,9 +546,7 @@ export default {
rules: {
settlementIds: [{ validator: this.validateSettlements, trigger: 'change' }],
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
departmentEmails: [
{ type: 'array', required: true, min: 1, message: '请选择部门邮箱', trigger: 'change' },
],
departmentEmails: [{ validator: this.validateDepartmentEmails, trigger: 'change' }],
receiverInvoiceInfoId: [{ required: true, message: '请选择受票方单位', trigger: 'change' }],
contactPhone: [{ pattern: /^\d{11}$/, message: '联系电话必须为11位数字', trigger: 'blur' }],
taxpayerNo: [{ validator: this.validateInvoiceInfoField, trigger: 'change' }],
@@ -614,11 +592,19 @@ export default {
0
);
},
allocatedTotal() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.allocatedInvoiceAmount || 0),
0
);
detailColumns() {
return [...this.detailBaseColumns, ...this.feeItemColumns];
},
invoiceItemCategories() {
return [...new Set(this.invoiceItems.map(item => item.categoryName).filter(Boolean))];
},
taxRateOptions() {
const rates = new Set([0, 1, 3, 6, 9, 13]);
this.invoiceItems.forEach(item => {
const rate = Number(item.defaultTaxRate);
if (Number.isFinite(rate) && rate >= 0 && rate <= 100) rates.add(rate);
});
return [...rates].sort((left, right) => left - right);
},
},
created() {
@@ -641,22 +627,25 @@ export default {
return [];
}
},
parseEmails(value) {
const values = Array.isArray(value) ? value : [value];
return values
.flatMap(item => String(item || '').split(/[;,,;]/))
.map(item => item.trim())
.filter(Boolean);
},
async initialize() {
await Promise.all([this.loadInvoiceItems(), this.loadTransportTypeOptions()]);
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),
departmentEmails: this.parseEmails(data.departmentEmails),
settlements: (data.settlements || []).map(item => ({
...item,
settlementId: item.formalSettlementId,
@@ -669,12 +658,76 @@ export default {
attachments: this.parse(data.attachmentsJson),
};
if (!this.form.sheets.length) this.form.sheets = [emptySheet()];
this.form.sheets.forEach(sheet =>
sheet.lines.forEach(line => this.syncInvoiceItemLine(line, false))
);
this.selectedDetailIds = (data.details || []).map(item => item.formalSettlementDetailId);
if (this.settlementIds.length) {
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
}
if (this.readonly) this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
this.$nextTick(this.restoreDetailSelection);
},
async loadInvoiceItems() {
const data = this.unwrapData(await getInvoiceItems());
this.invoiceItems = Array.isArray(data) ? data : data?.records || [];
this.form.sheets.forEach(sheet =>
sheet.lines.forEach(line => this.syncInvoiceItemLine(line, false))
);
},
async loadTransportTypeOptions() {
const data = this.unwrapData(await getDictionary({ code: 'transport_type' }));
const records = Array.isArray(data) ? data : data?.records || [];
this.transportTypeOptions = records.map(item => ({
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
},
transportTypeLabel(value) {
if (!value) return '-';
return (
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label ||
value
);
},
invoiceItemNames(categoryName) {
return this.invoiceItems.filter(item => item.categoryName === categoryName);
},
findInvoiceItem(line) {
return this.invoiceItems.find(
item => item.categoryName === line.goodsCategory && item.shortName === line.goodsName
);
},
syncInvoiceItemLine(line, clearInvalid = true) {
const categoryExists = this.invoiceItemCategories.includes(line.goodsCategory);
if (!categoryExists && clearInvalid) line.goodsCategory = '';
const item = this.findInvoiceItem(line);
if (item) {
line.taxRate = Number(item.defaultTaxRate || 0);
this.recalculateLine(line);
} else if (clearInvalid) {
line.goodsName = '';
line.taxRate = 0;
}
},
handleGoodsCategoryChange(line) {
const names = this.invoiceItemNames(line.goodsCategory);
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
line.taxRate = 0;
if (names.length === 1) {
line.goodsName = names[0].shortName;
this.handleGoodsNameChange(line);
}
},
handleGoodsNameChange(line) {
const item = this.findInvoiceItem(line);
if (!item) {
line.taxRate = 0;
return;
}
line.taxRate = Number(item.defaultTaxRate || 0);
this.recalculateLine(line);
},
validateSettlements(rule, value, callback) {
if (!this.form.settlements.length) {
callback(new Error('请选择正式结算单'));
@@ -715,6 +768,27 @@ export default {
}
callback();
},
validateDepartmentEmails(rule, value, callback) {
if (this.readonly) {
callback();
return;
}
const emails = this.parseEmails(value);
if (!emails.length) {
callback(new Error('请选择部门邮箱'));
return;
}
if (emails.length > 3) {
callback(new Error('部门邮箱最多选择3个'));
return;
}
const options = new Set(this.departmentEmailOptions.map(item => item.toLowerCase()));
if (emails.some(email => !options.has(email.toLowerCase()))) {
callback(new Error('部门邮箱必须选择当前客商开票信息中的邮箱'));
return;
}
callback();
},
handleInvoiceTypeChange() {
this.$nextTick(() => {
this.$refs.formRef?.validateField([
@@ -746,6 +820,12 @@ export default {
this.$message.warning('请至少选择一张应付正式结算单');
return;
}
await this.applySettlements(rows);
},
async selectSettlement(row) {
await this.applySettlements([row]);
},
async applySettlements(rows) {
const first = rows[0];
const incompatible = rows.some(
item =>
@@ -775,22 +855,83 @@ export default {
async loadSettlementDetails(resetSelection = true) {
this.detailRows =
this.unwrapData(await api.getSettlementDetails(this.settlementIds.join(','))) || [];
this.buildFeeItemColumns();
if (resetSelection) this.selectedDetailIds = this.detailRows.map(item => item.id);
this.$nextTick(this.restoreDetailSelection);
},
parseFeeItems(value) {
if (!value) return {};
if (typeof value === 'object' && !Array.isArray(value)) return value;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
},
buildFeeItemColumns() {
const groupedNames = new Map();
this.detailRows.forEach(row => {
Object.keys(this.parseFeeItems(row.feeItemsJson || row.feeItems)).forEach(name => {
const normalizedName = this.normalizeFeeItemName(name);
if (!normalizedName) return;
const key = this.feeItemNameKey(name);
const group = groupedNames.get(key) || { label: normalizedName, keys: [] };
if (!group.keys.includes(name)) group.keys.push(name);
groupedNames.set(key, group);
});
});
this.feeItemColumns = [...groupedNames.values()].map((group, index) => ({
prop: `feeItem_${index}`,
label: group.label,
minWidth: 130,
money: true,
feeItemKeys: group.keys,
}));
},
normalizeFeeItemName(name) {
return String(name || '')
.replace(/[\s\u200b-\u200d\ufeff]/g, '')
.trim();
},
feeItemNameKey(name) {
return this.normalizeFeeItemName(name).toLowerCase();
},
getFeeItemValue(row, names) {
const items = this.parseFeeItems(row.feeItemsJson || row.feeItems);
return names.reduce((total, name) => total + Number(items[name] || 0), 0);
},
async loadReceiverInformation() {
const data = this.unwrapData(await api.getReceiverInformation(this.settlementIds.join(',')));
this.receiverInvoiceOptions = data.invoiceInfos || [];
const invoiceInfos = data.invoices || data.invoiceInfos || data.customer?.invoices || [];
this.receiverInvoiceOptions = Array.isArray(invoiceInfos) ? invoiceInfos : [];
this.contactOptions = data.contacts || [];
const invoiceInfoEmails = [
...this.receiverInvoiceOptions.flatMap(item => this.parseEmails(item.email)),
];
this.departmentEmailOptions = invoiceInfoEmails
.filter(Boolean)
.reduce((emails, email) => {
if (!emails.some(item => item.toLowerCase() === email.toLowerCase())) emails.push(email);
return emails;
}, []);
this.form.departmentEmails = this.form.departmentEmails
.filter(email =>
this.departmentEmailOptions.some(option => option.toLowerCase() === email.toLowerCase())
)
.slice(0, 3);
this.form.issuerName = data.issuerName || this.form.issuerName;
if (!this.form.receiverInvoiceInfoId) {
const invoiceInfo =
let invoiceInfo = this.receiverInvoiceOptions.find(
item => String(item.id) === String(this.form.receiverInvoiceInfoId)
);
if (!invoiceInfo) {
invoiceInfo =
this.receiverInvoiceOptions.find(item => item.isDefault === 1) ||
this.receiverInvoiceOptions[0];
if (invoiceInfo) {
this.form.receiverInvoiceInfoId = invoiceInfo.id;
this.handleReceiverChange(invoiceInfo.id);
}
}
if (invoiceInfo) {
this.form.receiverInvoiceInfoId = invoiceInfo.id;
this.handleReceiverChange(invoiceInfo.id);
}
if (!this.form.contactName) {
const contact =
@@ -846,23 +987,9 @@ export default {
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 === '运费');
@@ -968,12 +1095,6 @@ export default {
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;
}
@@ -997,17 +1118,6 @@ export default {
: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;
+5 -7
View File
@@ -47,8 +47,7 @@
<section class="invoice-page__table-panel">
<div class="invoice-page__toolbar">
<div class="invoice-page__table-title">开票申请单列表</div>
<div class="invoice-page__toolbar-actions">
<div class="invoice-page__toolbar-left">
<el-button
v-if="hasPermission('invoice_application_sync')"
type="primary"
@@ -69,6 +68,8 @@
@click="handleExport"
>导出</el-button
>
</div>
<div class="invoice-page__toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button text :icon="Refresh" @click="loadTable" />
</el-tooltip>
@@ -442,16 +443,13 @@ export default {
align-items: center;
padding: 12px 0;
}
.invoice-page__toolbar-actions,
.invoice-page__toolbar-left,
.invoice-page__toolbar-right,
.invoice-page__links {
display: flex;
align-items: center;
gap: 8px;
}
.invoice-page__table-title {
font-size: 16px;
font-weight: 600;
}
.invoice-page__links {
flex-wrap: wrap;
justify-content: center;
+122 -181
View File
@@ -83,66 +83,6 @@
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户行">
<el-input v-model="form.bankName" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户账号">
<el-input v-model="form.bankAccount" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票行">
<el-input v-model="form.issuingBank" disabled placeholder="从金蝶票据池带出" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电话">
<el-input v-model="form.phone" :disabled="readonly" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客户邮箱" prop="customerEmails">
<el-select
v-model="form.customerEmails"
:disabled="readonly"
multiple
filterable
allow-create
default-first-option
:multiple-limit="3"
>
<el-option
v-for="email in customerEmailOptions"
:key="email"
:label="email"
:value="email"
/>
</el-select>
</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>
@@ -178,52 +118,7 @@
</section-card>
<section-card title="附件信息">
<template #extra>
<el-button
v-if="form.attachments.length"
type="primary"
plain
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
>
</template>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">{{ row.originalName || row.name || '-' }}</template>
</el-table-column>
<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="120" align="center">
<template #default="{ row, $index }">
<div class="receipt-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>
<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="上传附件"
class="receipt-form-page__uploader"
@change="normalizeAttachments"
/>
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card>
<section-card title="备注">
@@ -289,6 +184,11 @@
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="confirmInvoice(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="invoiceDialog.visible = false">取消</el-button>
@@ -331,6 +231,11 @@
<el-table-column label="已收票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectSettlement(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="settlementDialog.visible = false">取消</el-button>
@@ -341,9 +246,70 @@
</template>
<script>
import { Download, Search } from '@element-plus/icons-vue';
import { Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const invoicePoolMockRows = [
{
id: 90000001,
invoiceNo: '25312000000000000101',
invoiceDate: '2026-08-25',
invoiceType: '增值税专用发票',
taxRate: 9,
invoiceAmount: 109000,
taxAmount: 9000,
receiverName: '华东供应链管理有限公司',
issuerName: '上海安捷运输有限公司',
bankName: '中国工商银行上海分行',
bankAccount: '1001000100000000019',
issuingBank: '中国工商银行上海分行营业部',
phone: '021-66880001',
customerEmails: 'finance@huadong.example.com',
departmentEmails: 'transport@huadong.example.com',
attachmentsJson: '[]',
kingdeeBillNo: 'KD-FP-20260825001',
},
{
id: 90000002,
invoiceNo: '25312000000000000102',
invoiceDate: '2026-08-26',
invoiceType: '增值税普通发票',
taxRate: 6,
invoiceAmount: 53000,
taxAmount: 3000,
receiverName: '华东供应链管理有限公司',
issuerName: '江苏联运物流有限公司',
bankName: '中国建设银行南京分行',
bankAccount: '3201000100000000026',
issuingBank: '中国建设银行南京城南支行',
phone: '025-66880002',
customerEmails: 'finance@huadong.example.com',
departmentEmails: 'logistics@huadong.example.com',
attachmentsJson: '[]',
kingdeeBillNo: 'KD-FP-20260826002',
},
{
id: 90000003,
invoiceNo: '25312000000000000103',
invoiceDate: '2026-08-27',
invoiceType: '增值税专用发票',
taxRate: 3,
invoiceAmount: 20600,
taxAmount: 600,
receiverName: '华东供应链管理有限公司',
issuerName: '浙江通达物流有限公司',
bankName: '招商银行杭州分行',
bankAccount: '5719000100000000033',
issuingBank: '招商银行杭州高新支行',
phone: '0571-66880003',
customerEmails: 'finance@huadong.example.com',
departmentEmails: 'settlement@huadong.example.com',
attachmentsJson: '[]',
kingdeeBillNo: 'KD-FP-20260827003',
},
];
const emptyForm = () => ({
id: null,
@@ -359,6 +325,8 @@ const emptyForm = () => ({
bankName: '',
bankAccount: '',
issuingBank: '',
kingdeeBillNo: '',
kingdeeStatus: 'unsynced',
phone: '',
customerEmails: [],
departmentEmails: [],
@@ -373,29 +341,15 @@ const emptyForm = () => ({
export default {
name: 'InvoiceReceiptForm',
components: {
VehicleAttachmentTable,
},
data() {
return {
Download,
Search,
form: emptyForm(),
customerEmailOptions: [],
departmentEmailOptions: [],
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
invoiceDialog: { visible: false, loading: false, keyword: '', rows: [], current: null },
settlementDialog: {
visible: false,
@@ -433,12 +387,6 @@ export default {
settlementIds() {
return this.form.settlements.map(item => item.formalSettlementId || item.settlementId);
},
allocatedTotal() {
return this.form.settlements.reduce(
(total, item) => total + Number(item.allocatedInvoiceAmount || 0),
0
);
},
},
created() {
this.initialize();
@@ -523,25 +471,37 @@ export default {
async loadInvoicePool() {
this.invoiceDialog.loading = true;
try {
this.invoiceDialog.rows =
this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword)) || [];
if (!this.recordId) {
this.invoiceDialog.rows = this.filterInvoicePoolMockRows(this.invoiceDialog.keyword);
return;
}
const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword));
this.invoiceDialog.rows = Array.isArray(data) ? data : data?.records || [];
} finally {
this.invoiceDialog.loading = false;
}
},
filterInvoicePoolMockRows(keyword) {
const normalizedKeyword = String(keyword || '')
.trim()
.toLowerCase();
if (!normalizedKeyword) return invoicePoolMockRows.map(item => ({ ...item }));
return invoicePoolMockRows
.filter(item =>
[item.invoiceNo, item.issuerName, item.receiverName].some(value =>
String(value || '')
.toLowerCase()
.includes(normalizedKeyword)
)
)
.map(item => ({ ...item }));
},
confirmInvoice(row, options = {}) {
const selected = row?.id ? row : this.invoiceDialog.current;
if (!selected) {
this.$message.warning('请选择一张金蝶票据池发票');
return;
}
if (this.form.settlements.length) {
const first = this.form.settlements[0];
if (selected.receiverName !== first.payerName || selected.issuerName !== first.payeeName) {
this.$message.warning('金蝶发票的开票单位、受票单位与结算单收付款方不一致');
return;
}
}
const preserveUserInput = options.preserveUserInput === true;
Object.assign(this.form, {
kingdeeInvoicePoolId: selected.id,
@@ -556,6 +516,8 @@ export default {
bankName: selected.bankName,
bankAccount: selected.bankAccount,
issuingBank: selected.issuingBank,
kingdeeBillNo: selected.kingdeeBillNo || '',
kingdeeStatus: selected.kingdeeStatus || 'unsynced',
phone: preserveUserInput ? this.form.phone : selected.phone || '',
customerEmails: preserveUserInput
? this.form.customerEmails
@@ -608,13 +570,6 @@ export default {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
if (
this.form.kingdeeInvoicePoolId &&
(this.form.receiverName !== first.payerName || this.form.issuerName !== first.payeeName)
) {
this.$message.warning('金蝶发票的开票单位、受票单位与结算单收付款方不一致');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
@@ -629,6 +584,10 @@ export default {
await this.loadReferenceInformation();
this.$refs.formRef?.validateField('settlementIds').catch(() => {});
},
async selectSettlement(row) {
this.settlementDialog.selection = [row];
await this.confirmSettlements();
},
async loadReferenceInformation() {
const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(',')));
this.customerEmailOptions = data.customerEmails || [];
@@ -645,28 +604,6 @@ export default {
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,
}));
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
openSettlementDetail(row) {
const id = row.formalSettlementId || row.settlementId || row.id;
if (!id) return;
@@ -678,9 +615,6 @@ export default {
validateBusiness() {
const invoiceAmount = Number(this.form.invoiceAmount || 0);
if (invoiceAmount <= 0) throw new Error('开票金额必须大于0');
if (Math.abs(this.allocatedTotal - invoiceAmount) > 0.001) {
throw new Error('分摊发票金额总和必须等于开票金额');
}
for (const row of this.form.settlements) {
const allocated = Number(row.allocatedInvoiceAmount);
if (!Number.isFinite(allocated) || allocated < 0) {
@@ -695,6 +629,19 @@ export default {
return {
id: this.form.id,
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
invoiceNo: this.form.invoiceNo,
invoiceDate: this.form.invoiceDate,
invoiceType: this.form.invoiceType,
taxRate: this.form.taxRate,
invoiceAmount: this.form.invoiceAmount,
taxAmount: this.form.taxAmount,
receiverName: this.form.receiverName,
issuerName: this.form.issuerName,
bankName: this.form.bankName,
bankAccount: this.form.bankAccount,
issuingBank: this.form.issuingBank,
kingdeeBillNo: this.form.kingdeeBillNo,
kingdeeStatus: this.form.kingdeeStatus,
phone: this.form.phone,
customerEmails: this.form.customerEmails.join(';'),
departmentEmails: this.form.departmentEmails.join(';'),
@@ -731,7 +678,10 @@ export default {
this.$message.warning('请先选择金蝶票据池发票');
return;
}
const rows = this.unwrapData(await api.getInvoicePool(this.form.invoiceNo)) || [];
const data = this.recordId
? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo))
: this.filterInvoicePoolMockRows(this.form.invoiceNo);
const rows = Array.isArray(data) ? data : data?.records || [];
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
if (!invoice) {
this.$message.warning('金蝶票据池中未查询到该发票');
@@ -762,18 +712,9 @@ export default {
.receipt-form-page__form :deep(.el-input-number) {
width: 100%;
}
.receipt-form-page__links {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.receipt-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
.receipt-form-page__uploader {
margin-top: 12px;
}
.receipt-form-page__actions {
display: flex;
justify-content: flex-end;
+5 -7
View File
@@ -56,8 +56,7 @@
<section class="receipt-page__table-panel">
<div class="receipt-page__toolbar">
<div class="receipt-page__table-title">收票单列表</div>
<div class="receipt-page__toolbar-actions">
<div class="receipt-page__toolbar-left">
<el-button
v-if="hasPermission('invoice_receipt_sync')"
type="primary"
@@ -75,6 +74,8 @@
@click="handleExport"
>导出</el-button
>
</div>
<div class="receipt-page__toolbar-right">
<el-tooltip content="刷新" placement="top">
<el-button text :icon="Refresh" @click="loadTable" />
</el-tooltip>
@@ -444,11 +445,8 @@ export default {
align-items: center;
padding: 12px 0;
}
.receipt-page__table-title {
font-size: 16px;
font-weight: 600;
}
.receipt-page__toolbar-actions,
.receipt-page__toolbar-left,
.receipt-page__toolbar-right,
.receipt-page__links {
display: flex;
align-items: center;
+9 -105
View File
@@ -96,51 +96,11 @@
</el-table>
</section-card>
<section-card title="附件信息"><template #extra><el-button
v-if="form.attachments.length"
type="primary"
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
></template>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220">
<template #default="{ row }">{{ row.originalName || row.name || '-' }}</template>
</el-table-column>
<el-table-column prop="description" label="附件描述" min-width="240" />
<el-table-column label="文件大小" width="120">
<template #default="{ row }">{{ displayFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" />
<el-table-column prop="uploadTime" label="上传时间" width="170" />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<div class="claim-record-form-page__links">
<el-link v-if="row.url || row.link" type="primary" @click="viewAttachment(row)">
查看
</el-link>
<el-link
v-if="form.claimStatus === 'claimed'"
type="danger"
@click="removeAttachment(row)"
>删除</el-link
>
</div>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-if="form.claimStatus === 'claimed'"
<section-card title="附件信息">
<vehicle-attachment-table
v-model="form.attachments"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
class="claim-record-form-page__uploader"
@change="handleAttachmentChange"
:readonly="form.claimStatus !== 'claimed'"
@update:model-value="handleAttachmentUpdate"
/>
</section-card>
@@ -156,8 +116,8 @@
</template>
<script>
import { Download } from '@element-plus/icons-vue';
import * as api from '@/api/payment/receiptClaimRecord';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const emptyForm = () => ({
receiptNoticeNo: '',
@@ -182,26 +142,10 @@ const emptyForm = () => ({
export default {
name: 'ReceiptClaimRecordForm',
components: { VehicleAttachmentTable },
data() {
return {
Download,
form: emptyForm(),
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
};
},
computed: {
@@ -241,40 +185,9 @@ export default {
return [];
}
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
viewAttachment(row) {
window.open(row.url || row.link, '_blank', 'noopener');
},
async handleAttachmentChange(files) {
const userInfo = this.$store.getters.userInfo || {};
const userName = userInfo.realName || userInfo.userName || '';
const uploadTime = 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 || uploadTime,
}));
await this.persistAttachments('附件上传成功');
},
async removeAttachment(row) {
await this.$confirm('确定删除该附件吗?', '删除附件', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
});
this.form.attachments = this.form.attachments.filter(item => item !== row);
await this.persistAttachments('附件删除成功');
async handleAttachmentUpdate(files) {
this.form.attachments = files || [];
await this.persistAttachments('附件更新成功');
},
async persistAttachments(message) {
await api.updateAttachments({
@@ -296,15 +209,6 @@ export default {
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
displayFileSize(size) {
if (size === null || size === undefined || size === '') return '-';
if (typeof size === 'string' && /[a-z]/i.test(size)) return size;
const bytes = Number(size);
if (!Number.isFinite(bytes)) return size;
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
},
},
};
</script>
+30 -112
View File
@@ -108,50 +108,8 @@
</el-table>
</section-card>
<section-card title="附件信息"><template #extra><el-button
v-if="form.attachments.length"
type="primary"
plain
:icon="Download"
@click="downloadAttachments"
>批量下载</el-button
></template>
<el-table :data="form.attachments" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column label="文件名" min-width="220"
><template #default="{ row }">{{
row.originalName || row.name || '-'
}}</template></el-table-column
>
<el-table-column label="附件描述" min-width="240"
><template #default="{ row }"
><el-input v-model="row.description" 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="120" align="center"
><template #default="{ row, $index }"
><div class="receipt-claim-form-page__links">
<el-link type="primary" :href="row.url || row.link" target="_blank">查看</el-link
><el-link type="danger" @click="form.attachments.splice($index, 1)">删除</el-link>
</div></template
></el-table-column
>
</el-table>
<vehicle-attachment-upload
v-model="form.attachments"
:multiple="true"
:limit="20"
:max-size="500"
:file-types="attachmentFileTypes"
:show-file-list="false"
button-text="上传附件"
class="receipt-claim-form-page__uploader"
@change="normalizeAttachments"
/>
<section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" />
</section-card>
<section-card title="备注">
@@ -190,27 +148,27 @@
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="payerName" label="付款方" min-width="150" /><el-table-column
prop="payeeName"
label="收款方"
min-width="150"
/><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="150" align="right"
><template #default="{ row }">{{
formatMoney(row.claimedReceiptAmount)
}}</template></el-table-column
>
<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="payerName" label="付款方" min-width="150" />
<el-table-column prop="payeeName" label="收款方" min-width="150" />
<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="150" align="right">
<template #default="{ row }">
{{ formatMoney(row.claimedReceiptAmount) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click.stop="selectSettlement(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer
><el-button @click="settlementDialog.visible = false">取消</el-button
@@ -221,8 +179,9 @@
</template>
<script>
import { Download, Search } from '@element-plus/icons-vue';
import { Search } from '@element-plus/icons-vue';
import * as api from '@/api/payment/receiptFlow';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const emptyForm = () => ({
id: null,
@@ -247,28 +206,12 @@ const emptyForm = () => ({
export default {
name: 'ReceiptFlowForm',
components: { VehicleAttachmentTable },
data() {
return {
Download,
Search,
form: emptyForm(),
submitting: false,
attachmentFileTypes: [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
],
settlementDialog: { visible: false, loading: false, keyword: '', rows: [], selection: [] },
rules: {
settlementIds: [
@@ -359,13 +302,6 @@ export default {
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
return;
}
if (
this.form.counterpartyName &&
rows.some(item => item.payerName && item.payerName !== this.form.counterpartyName)
) {
this.$message.warning('对方户名与结算单付款方不一致');
return;
}
this.form.settlements = rows.map(item => ({
...item,
formalSettlementId: item.id,
@@ -376,31 +312,13 @@ export default {
this.settlementDialog.visible = false;
this.$nextTick(() => this.$refs.formRef?.validateField('settlementIds'));
},
selectSettlement(row) {
this.settlementDialog.selection = [row];
this.confirmSettlements();
},
remainingAmount(row) {
return Math.max(Number(row.settlementAmount || 0) - Number(row.claimedReceiptAmount || 0), 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,
}));
},
downloadAttachments() {
this.form.attachments.forEach(file => {
const url = file.url || file.link;
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.originalName || file.name || '';
anchor.target = '_blank';
anchor.click();
});
},
async submitClaim() {
await this.$refs.formRef.validate();
const amount = Number(this.form.receiptAmount || 0);
+31 -3
View File
@@ -52,7 +52,6 @@
<section class="receipt-flow-page__table-panel">
<div class="receipt-flow-page__toolbar">
<div />
<el-button
v-if="hasPermission('receipt_flow_sync')"
type="primary"
@@ -110,6 +109,33 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptFlow';
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
const receiptFlowMockRows = [
{
receiptNoticeNo: 'SKTZ20260827001',
payerName: '华东供应链管理有限公司',
receiptAmount: 48000,
counterpartyName: '上海安捷运输有限公司',
counterpartyAccount: '310000000000001001',
counterpartyBank: '中国工商银行上海分行',
summary: '运输服务款',
transactionTime: '2026-08-27 09:30:00',
detailSerialNo: 'MOCK-RF-20260827-001',
sourceUpdatedTime: '2026-08-27 09:30:00',
},
{
receiptNoticeNo: 'SKTZ20260827002',
payerName: '华东供应链管理有限公司',
receiptAmount: 26500,
counterpartyName: '江苏联运物流有限公司',
counterpartyAccount: '320100000000002002',
counterpartyBank: '中国建设银行南京分行',
summary: '物流服务款',
transactionTime: '2026-08-27 13:45:00',
detailSerialNo: 'MOCK-RF-20260827-002',
sourceUpdatedTime: '2026-08-27 13:45:00',
},
];
const emptyQuery = () => ({
receiptNoticeNo: '',
counterpartyName: '',
@@ -184,7 +210,9 @@ export default {
async handleSync() {
this.syncing = true;
try {
const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
const syncedCount = Number(
this.unwrapData(await api.sync({ flows: receiptFlowMockRows })) || 0
);
this.$message.success(`流水同步完成,共同步${syncedCount}`);
await this.loadTable();
} finally {
@@ -246,7 +274,7 @@ export default {
}
.receipt-flow-page__toolbar {
display: flex;
justify-content: space-between;
justify-content: flex-start;
align-items: center;
padding: 12px 0;
}