增加票通票样功能
This commit is contained in:
@@ -705,6 +705,28 @@ export function invoiceDetailApi(invoiceReqSerialNo: string): Promise<InvoiceDet
|
||||
return http.get('/pt/invoiceDetail', { params: { invoiceReqSerialNo } })
|
||||
}
|
||||
|
||||
export function invoiceDownloadUrlApi(invoiceReqSerialNo: string): Promise<{ downloadUrl?: string }> {
|
||||
return http.get('/pt/invoiceDownloadUrl', { params: { invoiceReqSerialNo } })
|
||||
}
|
||||
|
||||
export function invoicePreviewBlobApi(invoiceReqSerialNo: string): Promise<Blob> {
|
||||
return http.get('/pt/invoicePreview', {
|
||||
params: { invoiceReqSerialNo },
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function redInvoiceDownloadUrlApi(invoiceReqSerialNo: string): Promise<{ downloadUrl?: string }> {
|
||||
return http.get('/pt/redInvoiceDownloadUrl', { params: { invoiceReqSerialNo } })
|
||||
}
|
||||
|
||||
export function redInvoicePreviewBlobApi(invoiceReqSerialNo: string): Promise<Blob> {
|
||||
return http.get('/pt/redInvoicePreview', {
|
||||
params: { invoiceReqSerialNo },
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询并刷新发票状态
|
||||
*/
|
||||
|
||||
@@ -361,6 +361,53 @@
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showSamplePreview"
|
||||
preset="card"
|
||||
title="查看票样"
|
||||
:style="{ width: '980px', maxWidth: '94vw' }"
|
||||
content-style="padding: 0"
|
||||
>
|
||||
<div class="sample-preview">
|
||||
<div class="sample-toolbar">
|
||||
<div class="sample-title">{{ sampleSerialNo }}</div>
|
||||
<div class="sample-actions">
|
||||
<n-button size="small" secondary @click="zoomOutSample">
|
||||
<template #icon><ZoomOut :size="14" /></template>
|
||||
缩小
|
||||
</n-button>
|
||||
<span class="sample-zoom">{{ sampleZoom }}%</span>
|
||||
<n-button size="small" secondary @click="zoomInSample">
|
||||
<template #icon><ZoomIn :size="14" /></template>
|
||||
放大
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
tag="a"
|
||||
:href="sampleBlobUrl || undefined"
|
||||
:download="`${sampleSerialNo || 'invoice'}.pdf`"
|
||||
:disabled="!sampleBlobUrl"
|
||||
>
|
||||
<template #icon><Download :size="14" /></template>
|
||||
下载
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-spin :show="sampleLoading">
|
||||
<div class="sample-frame-wrap">
|
||||
<iframe
|
||||
v-if="sampleBlobUrl"
|
||||
class="sample-frame"
|
||||
:src="sampleBlobUrl"
|
||||
:style="{ transform: `scale(${sampleZoom / 100})` }"
|
||||
/>
|
||||
<n-empty v-else description="暂无票样地址" />
|
||||
</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showRedForm"
|
||||
preset="card"
|
||||
@@ -410,6 +457,7 @@ import type { Component } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
@@ -426,16 +474,24 @@ import {
|
||||
Clock,
|
||||
XCircle,
|
||||
FileSpreadsheet,
|
||||
FileSearch,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Download,
|
||||
RotateCcw
|
||||
} from 'lucide-vue-next'
|
||||
import {
|
||||
invoiceDownloadUrlApi,
|
||||
invoicePreviewBlobApi,
|
||||
invoiceDetailApi,
|
||||
invoiceHistoryApi,
|
||||
invoiceKindMap,
|
||||
invoiceStatusMap,
|
||||
queryInvoiceApi,
|
||||
redInvoiceCreateApi,
|
||||
redInvoiceDownloadUrlApi,
|
||||
redInvoiceInfoApi,
|
||||
redInvoicePreviewBlobApi,
|
||||
redReasonMap
|
||||
} from '@/api/piaotong'
|
||||
import type {
|
||||
@@ -446,7 +502,7 @@ import type {
|
||||
RedCreateRequest,
|
||||
RedInvoiceInfo
|
||||
} from '@/api/piaotong'
|
||||
import type { DataTableColumn } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
const invoiceTypeMap: Record<string, string> = {
|
||||
BLUE: '蓝票',
|
||||
@@ -583,7 +639,7 @@ const pagination = reactive({
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
pageSlot: 7,
|
||||
prefix: ({ itemCount }: { itemCount: number }) => `共 ${itemCount} 条`
|
||||
prefix: ({ itemCount }: { itemCount?: number }) => `共 ${itemCount ?? 0} 条`
|
||||
})
|
||||
|
||||
async function fetchData() {
|
||||
@@ -613,12 +669,22 @@ function handlePageSizeChange(pageSize: number) {
|
||||
}
|
||||
|
||||
const refreshingSet = reactive(new Set<string>())
|
||||
const showSamplePreview = ref(false)
|
||||
const sampleLoading = ref(false)
|
||||
const sampleUrl = ref('')
|
||||
const sampleBlobUrl = ref('')
|
||||
const sampleSerialNo = ref('')
|
||||
const sampleZoom = ref(100)
|
||||
|
||||
function getRowActions(row: InvoiceHistoryItem) {
|
||||
const actions: Array<{ label: string; icon?: Component; onClick: () => void }> = []
|
||||
actions.push({ label: '详情', icon: Eye, onClick: () => showDetailInfo(row) })
|
||||
actions.push({ label: '刷新', icon: RefreshCw, onClick: () => refreshStatus(row) })
|
||||
|
||||
if ((activeTab.value === 'BLUE' || activeTab.value === 'RED') && row.status === 'SUCCESS') {
|
||||
actions.push({ label: '查看票样', icon: FileSearch, onClick: () => openSamplePreview(row) })
|
||||
}
|
||||
|
||||
if (
|
||||
activeTab.value === 'BLUE' &&
|
||||
row.status === 'SUCCESS' &&
|
||||
@@ -630,7 +696,47 @@ function getRowActions(row: InvoiceHistoryItem) {
|
||||
return actions
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
async function openSamplePreview(item: InvoiceHistoryItem) {
|
||||
showSamplePreview.value = true
|
||||
sampleLoading.value = true
|
||||
if (sampleBlobUrl.value) {
|
||||
URL.revokeObjectURL(sampleBlobUrl.value)
|
||||
}
|
||||
sampleUrl.value = ''
|
||||
sampleBlobUrl.value = ''
|
||||
sampleSerialNo.value = item.invoiceReqSerialNo
|
||||
sampleZoom.value = 100
|
||||
try {
|
||||
const isRedInvoice = activeTab.value === 'RED' || item.invoiceType === 'RED'
|
||||
const res = isRedInvoice
|
||||
? await redInvoiceDownloadUrlApi(item.invoiceReqSerialNo)
|
||||
: await invoiceDownloadUrlApi(item.invoiceReqSerialNo)
|
||||
sampleUrl.value = res.downloadUrl || ''
|
||||
if (!sampleUrl.value) {
|
||||
message.warning('暂无票样地址')
|
||||
return
|
||||
}
|
||||
const blob = isRedInvoice
|
||||
? await redInvoicePreviewBlobApi(item.invoiceReqSerialNo)
|
||||
: await invoicePreviewBlobApi(item.invoiceReqSerialNo)
|
||||
sampleBlobUrl.value = URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
|
||||
} catch {
|
||||
message.error('预览票样失败')
|
||||
} finally {
|
||||
sampleLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function zoomInSample() {
|
||||
sampleZoom.value = Math.min(200, sampleZoom.value + 10)
|
||||
}
|
||||
|
||||
function zoomOutSample() {
|
||||
sampleZoom.value = Math.max(50, sampleZoom.value - 10)
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumns<InvoiceHistoryItem>>(() => {
|
||||
const tableColumns: DataTableColumns<InvoiceHistoryItem> = [
|
||||
{
|
||||
title: '流水号',
|
||||
key: 'invoiceReqSerialNo',
|
||||
@@ -659,7 +765,17 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
key: 'redFlag',
|
||||
width: 110,
|
||||
render: (row: InvoiceHistoryItem) => {
|
||||
if (!row.redFlag || row.redFlag === 'NOT_RED') {
|
||||
const redFlag =
|
||||
row.redFlag ||
|
||||
(row.invoiceType === 'RED'
|
||||
? {
|
||||
SUCCESS: 'ALREADY_RED',
|
||||
PROCESSING: 'REDING',
|
||||
PENDING: 'REDING',
|
||||
FAILED: 'RED_FAIL'
|
||||
}[row.status]
|
||||
: undefined)
|
||||
if (!redFlag || redFlag === 'NOT_RED') {
|
||||
return h(NTag, { size: 'small', round: true }, () => '未冲红')
|
||||
}
|
||||
const typeMap: Record<string, 'error' | 'warning' | 'default'> = {
|
||||
@@ -670,8 +786,8 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
}
|
||||
return h(
|
||||
NTag,
|
||||
{ size: 'small', round: true, type: typeMap[row.redFlag] || 'default' },
|
||||
() => redFlagMap[row.redFlag] || row.redFlag
|
||||
{ size: 'small', round: true, type: typeMap[redFlag] || 'default' },
|
||||
() => redFlagMap[redFlag] || redFlag
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -713,6 +829,7 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
{ style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap' },
|
||||
actions.map((btn) => {
|
||||
const isLoading = btn.label === '刷新' && refreshingSet.has(row.invoiceReqSerialNo)
|
||||
const Icon = btn.icon
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
@@ -723,14 +840,19 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
},
|
||||
{
|
||||
default: () => btn.label,
|
||||
...(btn.icon && !isLoading ? { icon: () => h(btn.icon, { size: 13 }) } : {})
|
||||
...(Icon && !isLoading ? { icon: () => h(Icon, { size: 13 }) } : {})
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
return activeTab.value === 'RED'
|
||||
? tableColumns.filter((column) => !('key' in column) || column.key !== 'redFlag')
|
||||
: tableColumns
|
||||
})
|
||||
|
||||
const showDetail = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
@@ -830,7 +952,7 @@ async function handleRedSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
const goodsColumns: DataTableColumn[] = [
|
||||
const goodsColumns: DataTableColumns<InvoiceDetailGoods> = [
|
||||
{ title: '行号', key: 'lineNo', width: 60, align: 'center' },
|
||||
{ title: '商品名称', key: 'goodsName', width: 140, ellipsis: { tooltip: true } },
|
||||
{ title: '税收分类编码', key: 'taxClassificationCode', width: 120, ellipsis: { tooltip: true } },
|
||||
@@ -931,7 +1053,7 @@ function goodsSummary() {
|
||||
]
|
||||
}
|
||||
|
||||
const voucherColumns: DataTableColumn[] = [
|
||||
const voucherColumns: DataTableColumns<InvoiceDetailVoucher> = [
|
||||
{
|
||||
title: '凭证类型',
|
||||
key: 'proofType',
|
||||
@@ -1045,6 +1167,56 @@ onMounted(() => {
|
||||
background: #fafafa !important;
|
||||
}
|
||||
|
||||
.sample-preview {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sample-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sample-title {
|
||||
color: #111827;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sample-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sample-zoom {
|
||||
min-width: 46px;
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sample-frame-wrap {
|
||||
height: 70vh;
|
||||
min-height: 420px;
|
||||
overflow: auto;
|
||||
border: 1px solid #eef1f5;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.sample-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 680px;
|
||||
border: 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.detail-shell {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user