435 lines
16 KiB
Vue
435 lines
16 KiB
Vue
<template>
|
||
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
|
||
<el-form :model="query" inline label-width="130px" @submit.prevent>
|
||
<el-form-item label="发票号码">
|
||
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="开票日期">
|
||
<el-date-picker
|
||
v-model="query.invoiceDateRange"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
format="YYYY-MM-DD"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="开票单位">
|
||
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="受票单位">
|
||
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="开票单位(销方)">
|
||
<el-select
|
||
v-model="query.salerTaxNo"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="keyword => searchCustomers(keyword, 'saler')"
|
||
:loading="customerLoading.saler"
|
||
placeholder="输入名称搜索或直接粘贴税号"
|
||
style="width: 220px"
|
||
@keyup.enter="handleQuery"
|
||
>
|
||
<el-option
|
||
v-for="item in customerOptions.saler"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="受票单位(购方)">
|
||
<el-select
|
||
v-model="query.buyerTaxNo"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="keyword => searchCustomers(keyword, 'buyer')"
|
||
:loading="customerLoading.buyer"
|
||
placeholder="输入名称搜索或直接粘贴税号"
|
||
style="width: 220px"
|
||
@keyup.enter="handleQuery"
|
||
>
|
||
<el-option
|
||
v-for="item in customerOptions.buyer"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="发票类型">
|
||
<el-select
|
||
v-model="query.invoiceType"
|
||
clearable
|
||
filterable
|
||
placeholder="不限"
|
||
style="width: 230px"
|
||
>
|
||
<el-option
|
||
v-for="option in invoiceTypeOptions"
|
||
:key="option.value"
|
||
:label="option.label"
|
||
:value="option.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="认领状态">
|
||
<el-select v-model="query.claimed" clearable placeholder="不限" style="width: 120px">
|
||
<el-option label="未认领" :value="false" />
|
||
<el-option label="已认领" :value="true" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="登记状态">
|
||
<el-select v-model="query.registered" clearable placeholder="不限" style="width: 120px">
|
||
<el-option label="未登记" :value="false" />
|
||
<el-option label="已登记" :value="true" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
|
||
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table
|
||
ref="poolTable"
|
||
v-loading="loading"
|
||
:data="rows"
|
||
:row-key="rowKey"
|
||
border
|
||
height="520"
|
||
:highlight-current-row="!multiple"
|
||
@selection-change="selection => (selected = selection)"
|
||
@row-dblclick="handleRowDblclick"
|
||
>
|
||
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
|
||
<el-table-column v-else width="52" align="center" fixed="left">
|
||
<template #default="{ row }">
|
||
<el-radio
|
||
:model-value="current && current.invoiceNo"
|
||
:value="row.invoiceNo"
|
||
@change="current = row"
|
||
>
|
||
<span></span>
|
||
</el-radio>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
|
||
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
|
||
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
|
||
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
|
||
<el-table-column label="金额(含税)" min-width="130" align="right">
|
||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="税率" min-width="90" align="center">
|
||
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="税额" min-width="120" align="right">
|
||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="剩余可用金额" min-width="130" align="right">
|
||
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
|
||
</el-table-column>
|
||
<!--<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />-->
|
||
<el-table-column label="认领状态" min-width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
|
||
<el-tag v-else type="info" size="small">未认领</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="登记状态" min-width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
|
||
<el-tag v-else type="info" size="small">未登记</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件" width="120" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-link
|
||
type="primary"
|
||
:underline="false"
|
||
:disabled="attachmentBusy === row.invoiceNo"
|
||
@click.stop="viewAttachment(row)"
|
||
>查看</el-link
|
||
>
|
||
<el-link
|
||
type="primary"
|
||
:underline="false"
|
||
:disabled="attachmentBusy === row.invoiceNo"
|
||
@click.stop="downloadAttachment(row)"
|
||
>下载</el-link
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="kingdee-pool-dialog__pager">
|
||
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
|
||
<el-pagination
|
||
v-model:current-page="page.current"
|
||
v-model:page-size="page.size"
|
||
:total="page.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:disabled="loading"
|
||
@current-change="load"
|
||
@size-change="handleSizeChange"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script>
|
||
import request from '@/axios';
|
||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||
import {
|
||
downloadKingdeeInvoiceFile,
|
||
isKingdeePathAttachment,
|
||
viewKingdeeInvoiceFile,
|
||
} from '@/utils/kingdee-attachment';
|
||
|
||
export default {
|
||
name: 'KingdeeInvoicePoolDialog',
|
||
props: {
|
||
modelValue: { type: Boolean, required: true },
|
||
title: { type: String, default: '查询金蝶票据池' },
|
||
confirmText: { type: String, default: '确定' },
|
||
multiple: { type: Boolean, default: false },
|
||
initialInvoiceNo: { type: String, default: '' },
|
||
queryApi: { type: Function, required: true },
|
||
},
|
||
emits: ['update:modelValue', 'confirm'],
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码(镜像按 invoice_type_code 精确匹配)
|
||
// 选票场景只开放数电票(普票/专票)
|
||
invoiceTypeOptions: [
|
||
{ value: '26', label: '数电发票(普通发票)' },
|
||
{ value: '27', label: '数电发票(增值税专用发票)' },
|
||
],
|
||
rows: [],
|
||
selected: [],
|
||
current: null,
|
||
page: { current: 1, size: 10, total: 0 },
|
||
// 正在查看/下载附件的发票号(行级 loading 守卫)
|
||
attachmentBusy: null,
|
||
// 销/购方客商下拉:远程按名称搜客商档案,选中带出税号查询;手动输入的裸税号直接作为值过滤
|
||
customerOptions: { saler: [], buyer: [] },
|
||
customerLoading: { saler: false, buyer: false },
|
||
query: this.emptyQuery(),
|
||
};
|
||
},
|
||
computed: {
|
||
visible: {
|
||
get() {
|
||
return this.modelValue;
|
||
},
|
||
set(value) {
|
||
this.$emit('update:modelValue', value);
|
||
},
|
||
},
|
||
},
|
||
methods: {
|
||
emptyQuery() {
|
||
return {
|
||
invoiceNo: '',
|
||
// 默认查近一个月,随请求传后端并在页面回显
|
||
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
|
||
salerName: '',
|
||
buyerName: '',
|
||
salerTaxNo: '',
|
||
buyerTaxNo: '',
|
||
invoiceType: '',
|
||
claimed: null,
|
||
registered: null,
|
||
};
|
||
},
|
||
// 镜像行主键为 id,回源入库前的响应仍可能无 id,用发票号码兜底
|
||
rowKey(row) {
|
||
return row.id || row.invoiceNo;
|
||
},
|
||
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
|
||
rowAttachment(row = {}) {
|
||
const value = row.attachmentsJson;
|
||
if (!value) return null;
|
||
let parsed = value;
|
||
if (typeof value === 'string') {
|
||
try {
|
||
parsed = JSON.parse(value);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
|
||
},
|
||
attachmentName(row) {
|
||
const attachment = this.rowAttachment(row);
|
||
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
|
||
},
|
||
async viewAttachment(row) {
|
||
const attachment = this.rowAttachment(row);
|
||
if (!isKingdeePathAttachment(attachment)) {
|
||
this.$message.warning('当前发票暂无可查看附件');
|
||
return;
|
||
}
|
||
this.attachmentBusy = row.invoiceNo;
|
||
try {
|
||
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||
} finally {
|
||
this.attachmentBusy = null;
|
||
}
|
||
},
|
||
async downloadAttachment(row) {
|
||
const attachment = this.rowAttachment(row);
|
||
if (!isKingdeePathAttachment(attachment)) {
|
||
this.$message.warning('当前发票暂无可下载附件');
|
||
return;
|
||
}
|
||
this.attachmentBusy = row.invoiceNo;
|
||
try {
|
||
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||
} finally {
|
||
this.attachmentBusy = null;
|
||
}
|
||
},
|
||
handleOpen() {
|
||
this.query = this.emptyQuery();
|
||
this.query.invoiceNo = this.initialInvoiceNo || '';
|
||
this.current = null;
|
||
this.selected = [];
|
||
this.$refs.poolTable?.clearSelection();
|
||
this.page.current = 1;
|
||
this.load();
|
||
},
|
||
handleQuery() {
|
||
this.page.current = 1;
|
||
this.load();
|
||
},
|
||
handleSizeChange() {
|
||
this.page.current = 1;
|
||
this.load();
|
||
},
|
||
resetQuery() {
|
||
this.query = this.emptyQuery();
|
||
this.handleQuery();
|
||
},
|
||
// 客商名称远程搜索:含 18 位社会信用代码特征(数字+大写字母)时按裸税号直查;
|
||
// 否则搜客商档案,取客商发票信息(blade_customer_invoice_info)的抬头/税号作选项——票池税号是开票税号,非统一信用代码
|
||
async searchCustomers(keyword, side) {
|
||
const text = String(keyword || '').trim();
|
||
if (!text) return;
|
||
if (/^[0-9A-Z]{15,20}$/.test(text)) {
|
||
this.customerOptions[side] = [{ label: `${text}(手输税号)`, value: text }];
|
||
return;
|
||
}
|
||
this.customerLoading[side] = true;
|
||
try {
|
||
const data = this.unwrap(await getCustomerList(1, 20, { fullName: text }));
|
||
const customers = (data.records || []).filter(item => item.id);
|
||
if (!customers.length) {
|
||
this.customerOptions[side] = [];
|
||
return;
|
||
}
|
||
// 列表接口不带发票信息,逐户取 detail(detail 组装 invoices,每户一张发票信息)
|
||
const options = await Promise.all(
|
||
customers.map(async customer => {
|
||
const detail = this.unwrap(await request({ url: '/blade-transport/customer-archive/detail', method: 'get', params: { id: customer.id } }));
|
||
return (detail?.invoices || [])
|
||
.filter(invoice => invoice.taxNo)
|
||
.map(invoice => ({ label: `${invoice.invoiceTitle || customer.fullName}(${invoice.taxNo})`, value: invoice.taxNo }));
|
||
})
|
||
);
|
||
this.customerOptions[side] = options.flat().filter(
|
||
(option, index, list) => list.findIndex(item => item.value === option.value) === index
|
||
);
|
||
} finally {
|
||
this.customerLoading[side] = false;
|
||
}
|
||
},
|
||
unwrap(response) {
|
||
const body = response?.data || response || {};
|
||
return body?.data || body;
|
||
},
|
||
async load() {
|
||
this.loading = true;
|
||
try {
|
||
const range = this.query.invoiceDateRange || [];
|
||
const data = this.unwrap(
|
||
await this.queryApi({
|
||
current: this.page.current,
|
||
size: this.page.size,
|
||
invoiceNo: this.query.invoiceNo || undefined,
|
||
invoiceDateStart: range[0] || undefined,
|
||
invoiceDateEnd: range[1] || undefined,
|
||
salerName: this.query.salerName || undefined,
|
||
buyerName: this.query.buyerName || undefined,
|
||
salerTaxNo: this.query.salerTaxNo || undefined,
|
||
buyerTaxNo: this.query.buyerTaxNo || undefined,
|
||
invoiceType: this.query.invoiceType || undefined,
|
||
claimed: this.query.claimed ?? undefined,
|
||
registered: this.query.registered ?? undefined,
|
||
})
|
||
);
|
||
this.rows = data.records || [];
|
||
this.page.total = Number(data.total || 0);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
handleRowDblclick(row) {
|
||
if (!this.multiple) {
|
||
this.confirmRows([row]);
|
||
}
|
||
},
|
||
handleConfirm() {
|
||
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
|
||
this.confirmRows(picked);
|
||
},
|
||
confirmRows(rows) {
|
||
if (!rows.length) {
|
||
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
|
||
return;
|
||
}
|
||
this.$emit('confirm', rows);
|
||
this.visible = false;
|
||
},
|
||
formatMoney(value) {
|
||
const number = Number(value);
|
||
return Number.isFinite(number) ? number.toFixed(2) : '-';
|
||
},
|
||
formatRate(value) {
|
||
const number = Number(value);
|
||
return Number.isFinite(number) ? `${number}%` : '-';
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.kingdee-pool-dialog__pager {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 12px;
|
||
gap: 16px;
|
||
}
|
||
.kingdee-pool-dialog__pager-tip {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|