增加票通票样功能

This commit is contained in:
BBIT-Kai
2026-05-19 17:43:39 +08:00
parent 74fdddd113
commit cdcfaa192c
14 changed files with 376 additions and 466 deletions
+1
View File
@@ -1,2 +1,3 @@
web/node_modules/ web/node_modules/
web/dist/ web/dist/
log/
-452
View File
File diff suppressed because one or more lines are too long
@@ -10,11 +10,13 @@ import com.bbit.ticket.database.piaotong.HistoryInvoiceVoucherTable
import com.bbit.ticket.entity.common.PageResult import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.request.AskInvoiceRequest import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.response.GetInvoiceInfoResponse import com.bbit.ticket.entity.response.GetInvoiceInfoResponse
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.InvoiceDetailGoods import com.bbit.ticket.entity.response.InvoiceDetailGoods
import com.bbit.ticket.entity.response.InvoiceDetailOrder import com.bbit.ticket.entity.response.InvoiceDetailOrder
import com.bbit.ticket.entity.response.InvoiceDetailResponse import com.bbit.ticket.entity.response.InvoiceDetailResponse
import com.bbit.ticket.entity.response.InvoiceDetailVoucher import com.bbit.ticket.entity.response.InvoiceDetailVoucher
import com.bbit.ticket.entity.response.InvoiceHistoryItem import com.bbit.ticket.entity.response.InvoiceHistoryItem
import com.bbit.ticket.utils.Base64TextUtil
import com.bbit.ticket.utils.formatDateTime import com.bbit.ticket.utils.formatDateTime
import org.jetbrains.exposed.v1.core.Column import org.jetbrains.exposed.v1.core.Column
import org.jetbrains.exposed.v1.core.Op import org.jetbrains.exposed.v1.core.Op
@@ -332,6 +334,7 @@ object BlueInvoiceDao {
it.setIfNotNull(HistoryInvoiceBasicTable.definedData, req.definedData) it.setIfNotNull(HistoryInvoiceBasicTable.definedData, req.definedData)
// 文件 // 文件
it.setIfNotNull(HistoryInvoiceBasicTable.invoiceLayoutFileType, req.invoiceLayoutFileType) it.setIfNotNull(HistoryInvoiceBasicTable.invoiceLayoutFileType, req.invoiceLayoutFileType)
it.setIfNotNull(HistoryInvoiceBasicTable.downloadUrl, Base64TextUtil.decodeToText(req.downloadUrl))
// 删除标记 // 删除标记
it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0" it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0"
} }
@@ -348,6 +351,18 @@ object BlueInvoiceDao {
?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息") ?: throw com.bbit.ticket.entity.common.BizException("NOT_FOUND", "发票记录不存在用户信息")
} }
fun invoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? {
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.userId eq userId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
.singleOrNull() ?: return null
return InvoiceDownloadUrlResponse(row[HistoryInvoiceBasicTable.downloadUrl])
}
/** /**
* 查询发票完整详情(含商品明细、差额征税凭证、关联单据) * 查询发票完整详情(含商品明细、差额征税凭证、关联单据)
*/ */
@@ -381,7 +396,7 @@ object BlueInvoiceDao {
invoiceAmount = row[HistoryInvoiceGoodsTable.itemAmount].toPlainString(), invoiceAmount = row[HistoryInvoiceGoodsTable.itemAmount].toPlainString(),
taxRateValue = row[HistoryInvoiceGoodsTable.taxRate].toPlainString(), taxRateValue = row[HistoryInvoiceGoodsTable.taxRate].toPlainString(),
taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount].toPlainString(), taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount].toPlainString(),
includeTaxFlag = false, includeTaxFlag = row[HistoryInvoiceGoodsTable.includeTaxFlag] == "1",
zeroTaxFlag = row[HistoryInvoiceGoodsTable.zeroTaxFlag], zeroTaxFlag = row[HistoryInvoiceGoodsTable.zeroTaxFlag],
preferentialPolicyFlag = row[HistoryInvoiceGoodsTable.preferentialPolicyFlag], preferentialPolicyFlag = row[HistoryInvoiceGoodsTable.preferentialPolicyFlag],
vatSpecialManage = row[HistoryInvoiceGoodsTable.vatSpecialManage], vatSpecialManage = row[HistoryInvoiceGoodsTable.vatSpecialManage],
@@ -5,6 +5,7 @@ package com.bbit.ticket.dao.piaotong
import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable import com.bbit.ticket.database.piaotong.HistoryInvoiceBasicTable
import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable import com.bbit.ticket.database.piaotong.HistoryInvoiceRedTable
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
@@ -46,6 +47,7 @@ object RedInvoiceDao {
it[HistoryInvoiceBasicTable.invoiceKind] = req.invoiceKind it[HistoryInvoiceBasicTable.invoiceKind] = req.invoiceKind
?: blueRow?.get(HistoryInvoiceBasicTable.invoiceKind) ?: "82" ?: blueRow?.get(HistoryInvoiceBasicTable.invoiceKind) ?: "82"
it[HistoryInvoiceBasicTable.invoiceType] = "2" // 红票 it[HistoryInvoiceBasicTable.invoiceType] = "2" // 红票
it[HistoryInvoiceBasicTable.redFlag] = "REDING"
// ---- 红冲关联 ---- // ---- 红冲关联 ----
it[HistoryInvoiceBasicTable.blueInvoiceCode] = blueRow?.get(HistoryInvoiceBasicTable.invoiceCode) it[HistoryInvoiceBasicTable.blueInvoiceCode] = blueRow?.get(HistoryInvoiceBasicTable.invoiceCode)
@@ -115,4 +117,17 @@ object RedInvoiceDao {
takerEmail = row[HistoryInvoiceRedTable.takerEmail], takerEmail = row[HistoryInvoiceRedTable.takerEmail],
) )
} }
fun invoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? {
val row = HistoryInvoiceBasicTable.selectAll()
.where {
(HistoryInvoiceBasicTable.userId eq userId) and
(HistoryInvoiceBasicTable.invoiceReqSerialNo eq invoiceReqSerialNo) and
(HistoryInvoiceBasicTable.invoiceType eq "2") and
HistoryInvoiceBasicTable.deletedAt.isNull()
}
.singleOrNull() ?: return null
return InvoiceDownloadUrlResponse(row[HistoryInvoiceBasicTable.downloadUrl])
}
} }
@@ -423,6 +423,7 @@ object HistoryInvoiceBasicTable : Table("history_invoice_basic") {
* 1:已删除 * 1:已删除
*/ */
val invDeletedFlag = varchar("inv_deleted_flag", 1) val invDeletedFlag = varchar("inv_deleted_flag", 1)
val downloadUrl = text("download_url").nullable()
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 时间字段 // 时间字段
@@ -130,7 +130,11 @@ object HistoryInvoiceGoodsTable : Table("history_invoice_goods") {
* 税额 * 税额
*/ */
val taxRateAmount = decimal("tax_rate_amount", 18, 2) val taxRateAmount = decimal("tax_rate_amount", 18, 2)
/**
* 税额
*/
val includeTaxFlag = varchar("include_tax_flag", 1)
.nullable()
/** /**
* 扣除额 * 扣除额
* *
@@ -0,0 +1,8 @@
package com.bbit.ticket.entity.response
import kotlinx.serialization.Serializable
@Serializable
data class InvoiceDownloadUrlResponse(
val downloadUrl: String? = null
)
@@ -12,8 +12,12 @@ import com.bbit.ticket.entity.request.RedCreateRequest
import com.bbit.ticket.service.piaotong.PTBlueService import com.bbit.ticket.service.piaotong.PTBlueService
import com.bbit.ticket.service.piaotong.PTRedService import com.bbit.ticket.service.piaotong.PTRedService
import com.bbit.ticket.utils.requireCurrentUser import com.bbit.ticket.utils.requireCurrentUser
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.server.request.receive import io.ktor.server.request.receive
import io.ktor.server.response.header
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.post import io.ktor.server.routing.post
@@ -93,6 +97,86 @@ fun Route.registerPTiInvoiceRoutes() {
call.respond(fail(code = "-1", message = e.message ?: "查询发票详情失败")) call.respond(fail(code = "-1", message = e.message ?: "查询发票详情失败"))
} }
} }
get("/invoiceDownloadUrl") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "璇蜂紶鍏ュ彂绁ㄨ姹傛祦姘村彿"))
return@get
}
val response = PTBlueService.getInvoiceDownloadUrl(currentUser.id, invoiceReqSerialNo)
if (response == null) {
call.respond(fail(code = "-1", message = "鏈壘鍒拌鍙戠エ璁板綍"))
return@get
}
call.respond(ok(response))
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "鏌ヨ鍙戠エ涓嬭浇鍦板潃澶辫触"))
}
}
get("/invoicePreview") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "璇蜂紶鍏ュ彂绁ㄨ姹傛祦姘村彿"))
return@get
}
val bytes = PTBlueService.getInvoicePreview(currentUser.id, invoiceReqSerialNo)
if (bytes == null) {
call.respond(fail(code = "-1", message = "鏈壘鍒扮エ鏍峰湴鍧€"))
return@get
}
call.response.header(
HttpHeaders.ContentDisposition,
"inline; filename=\"${invoiceReqSerialNo}.pdf\""
)
call.respondBytes(bytes, ContentType.Application.Pdf)
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "棰勮绁ㄦ牱澶辫触"))
}
}
get("/redInvoiceDownloadUrl") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "请传入发票请求流水号"))
return@get
}
val response = PTRedService.getRedInvoiceDownloadUrl(currentUser.id, invoiceReqSerialNo)
if (response == null) {
call.respond(fail(code = "-1", message = "未找到该红票记录"))
return@get
}
call.respond(ok(response))
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "查询红票下载地址失败"))
}
}
get("/redInvoicePreview") {
try {
val currentUser = call.requireCurrentUser()
val invoiceReqSerialNo = call.request.queryParameters["invoiceReqSerialNo"]
if (invoiceReqSerialNo.isNullOrBlank()) {
call.respond(fail(code = "-1", message = "请传入发票请求流水号"))
return@get
}
val bytes = PTRedService.getRedInvoicePreview(currentUser.id, invoiceReqSerialNo)
if (bytes == null) {
call.respond(fail(code = "-1", message = "未找到票样地址"))
return@get
}
call.response.header(
HttpHeaders.ContentDisposition,
"inline; filename=\"${invoiceReqSerialNo}.pdf\""
)
call.respondBytes(bytes, ContentType.Application.Pdf)
} catch (e: Exception) {
call.respond(fail(code = "-1", message = e.message ?: "预览红票票样失败"))
}
}
get("/queryInvoice") { get("/queryInvoice") {
try { try {
val currentUser = call.requireCurrentUser() val currentUser = call.requireCurrentUser()
@@ -7,10 +7,13 @@ import com.bbit.ticket.entity.common.PageResult
import com.bbit.ticket.entity.request.AskInvoiceRequest import com.bbit.ticket.entity.request.AskInvoiceRequest
import com.bbit.ticket.entity.request.QueryInvoiceRequest import com.bbit.ticket.entity.request.QueryInvoiceRequest
import com.bbit.ticket.entity.response.GetInvoiceInfoResponse import com.bbit.ticket.entity.response.GetInvoiceInfoResponse
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.QueryInvoiceResult import com.bbit.ticket.entity.response.QueryInvoiceResult
import com.bbit.ticket.entity.response.InvoiceCreateResponse import com.bbit.ticket.entity.response.InvoiceCreateResponse
import com.bbit.ticket.entity.response.InvoiceDetailResponse import com.bbit.ticket.entity.response.InvoiceDetailResponse
import com.bbit.ticket.entity.response.InvoiceHistoryItem import com.bbit.ticket.entity.response.InvoiceHistoryItem
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsBytes
import com.bbit.ticket.utils.plugins.dbQuery import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.net.PTClient import com.bbit.ticket.utils.net.PTClient
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -70,6 +73,16 @@ object PTBlueService {
suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? = suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? =
dbQuery { BlueInvoiceDao.invoiceDetail(userId, invoiceReqSerialNo) } dbQuery { BlueInvoiceDao.invoiceDetail(userId, invoiceReqSerialNo) }
suspend fun getInvoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery { BlueInvoiceDao.invoiceDownloadUrl(userId, invoiceReqSerialNo) }
suspend fun getInvoicePreview(userId: Uuid, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getInvoiceDownloadUrl(userId, invoiceReqSerialNo)?.downloadUrl
?.takeIf { it.isNotBlank() }
?: return null
return PTClient.client.get(downloadUrl).bodyAsBytes()
}
/** /**
* 查询并更新发票状态(复用 syncInvoiceFromPT * 查询并更新发票状态(复用 syncInvoiceFromPT
*/ */
@@ -6,11 +6,14 @@ import com.bbit.ticket.dao.piaotong.HistoryDao
import com.bbit.ticket.dao.piaotong.RedInvoiceDao import com.bbit.ticket.dao.piaotong.RedInvoiceDao
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
import com.bbit.ticket.entity.request.RedCreateRequest import com.bbit.ticket.entity.request.RedCreateRequest
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
import com.bbit.ticket.entity.response.QuickRedInvoiceResponse import com.bbit.ticket.entity.response.QuickRedInvoiceResponse
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
import com.bbit.ticket.utils.CurrentUser import com.bbit.ticket.utils.CurrentUser
import com.bbit.ticket.utils.plugins.dbQuery import com.bbit.ticket.utils.plugins.dbQuery
import com.bbit.ticket.utils.net.PTClient import com.bbit.ticket.utils.net.PTClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsBytes
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
/** /**
@@ -59,4 +62,13 @@ object PTRedService {
suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? = suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
dbQuery { RedInvoiceDao.findRedInfoBySerialNo(userId, invoiceReqSerialNo) } dbQuery { RedInvoiceDao.findRedInfoBySerialNo(userId, invoiceReqSerialNo) }
suspend fun getRedInvoiceDownloadUrl(userId: Uuid, invoiceReqSerialNo: String): InvoiceDownloadUrlResponse? =
dbQuery { RedInvoiceDao.invoiceDownloadUrl(userId, invoiceReqSerialNo) }
suspend fun getRedInvoicePreview(userId: Uuid, invoiceReqSerialNo: String): ByteArray? {
val downloadUrl = getRedInvoiceDownloadUrl(userId, invoiceReqSerialNo)?.downloadUrl
?.takeIf { it.isNotBlank() }
?: return null
return PTClient.client.get(downloadUrl).bodyAsBytes()
}
} }
@@ -0,0 +1,15 @@
package com.bbit.ticket.utils
import java.nio.charset.StandardCharsets
import java.util.Base64
object Base64TextUtil {
fun decodeToText(value: String?): String? {
val text = value?.trim() ?: return null
if (text.isEmpty()) return text
return runCatching {
String(Base64.getDecoder().decode(text), StandardCharsets.UTF_8)
}.getOrElse { text }
}
}
@@ -2,7 +2,7 @@ package com.bbit.ticket.utils.bootstrap
object Global { object Global {
val isDev = false val isDev = true
// 请求基础地址 // 请求基础地址
var baseUrl: String var baseUrl: String
+22
View File
@@ -705,6 +705,28 @@ export function invoiceDetailApi(invoiceReqSerialNo: string): Promise<InvoiceDet
return http.get('/pt/invoiceDetail', { params: { invoiceReqSerialNo } }) 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> </template>
</n-modal> </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 <n-modal
v-model:show="showRedForm" v-model:show="showRedForm"
preset="card" preset="card"
@@ -410,6 +457,7 @@ import type { Component } from 'vue'
import { import {
NButton, NButton,
NDataTable, NDataTable,
NEmpty,
NForm, NForm,
NFormItem, NFormItem,
NInput, NInput,
@@ -426,16 +474,24 @@ import {
Clock, Clock,
XCircle, XCircle,
FileSpreadsheet, FileSpreadsheet,
FileSearch,
ZoomIn,
ZoomOut,
Download,
RotateCcw RotateCcw
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { import {
invoiceDownloadUrlApi,
invoicePreviewBlobApi,
invoiceDetailApi, invoiceDetailApi,
invoiceHistoryApi, invoiceHistoryApi,
invoiceKindMap, invoiceKindMap,
invoiceStatusMap, invoiceStatusMap,
queryInvoiceApi, queryInvoiceApi,
redInvoiceCreateApi, redInvoiceCreateApi,
redInvoiceDownloadUrlApi,
redInvoiceInfoApi, redInvoiceInfoApi,
redInvoicePreviewBlobApi,
redReasonMap redReasonMap
} from '@/api/piaotong' } from '@/api/piaotong'
import type { import type {
@@ -446,7 +502,7 @@ import type {
RedCreateRequest, RedCreateRequest,
RedInvoiceInfo RedInvoiceInfo
} from '@/api/piaotong' } from '@/api/piaotong'
import type { DataTableColumn } from 'naive-ui' import type { DataTableColumns } from 'naive-ui'
const invoiceTypeMap: Record<string, string> = { const invoiceTypeMap: Record<string, string> = {
BLUE: '蓝票', BLUE: '蓝票',
@@ -583,7 +639,7 @@ const pagination = reactive({
showSizePicker: true, showSizePicker: true,
pageSizes: [10, 20, 50, 100], pageSizes: [10, 20, 50, 100],
pageSlot: 7, pageSlot: 7,
prefix: ({ itemCount }: { itemCount: number }) => `${itemCount}` prefix: ({ itemCount }: { itemCount?: number }) => `${itemCount ?? 0}`
}) })
async function fetchData() { async function fetchData() {
@@ -613,12 +669,22 @@ function handlePageSizeChange(pageSize: number) {
} }
const refreshingSet = reactive(new Set<string>()) 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) { function getRowActions(row: InvoiceHistoryItem) {
const actions: Array<{ label: string; icon?: Component; onClick: () => void }> = [] const actions: Array<{ label: string; icon?: Component; onClick: () => void }> = []
actions.push({ label: '详情', icon: Eye, onClick: () => showDetailInfo(row) }) actions.push({ label: '详情', icon: Eye, onClick: () => showDetailInfo(row) })
actions.push({ label: '刷新', icon: RefreshCw, onClick: () => refreshStatus(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 ( if (
activeTab.value === 'BLUE' && activeTab.value === 'BLUE' &&
row.status === 'SUCCESS' && row.status === 'SUCCESS' &&
@@ -630,7 +696,47 @@ function getRowActions(row: InvoiceHistoryItem) {
return actions 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: '流水号', title: '流水号',
key: 'invoiceReqSerialNo', key: 'invoiceReqSerialNo',
@@ -659,7 +765,17 @@ const columns = computed<DataTableColumn[]>(() => [
key: 'redFlag', key: 'redFlag',
width: 110, width: 110,
render: (row: InvoiceHistoryItem) => { 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 }, () => '未冲红') return h(NTag, { size: 'small', round: true }, () => '未冲红')
} }
const typeMap: Record<string, 'error' | 'warning' | 'default'> = { const typeMap: Record<string, 'error' | 'warning' | 'default'> = {
@@ -670,8 +786,8 @@ const columns = computed<DataTableColumn[]>(() => [
} }
return h( return h(
NTag, NTag,
{ size: 'small', round: true, type: typeMap[row.redFlag] || 'default' }, { size: 'small', round: true, type: typeMap[redFlag] || 'default' },
() => redFlagMap[row.redFlag] || row.redFlag () => redFlagMap[redFlag] || redFlag
) )
} }
}, },
@@ -713,6 +829,7 @@ const columns = computed<DataTableColumn[]>(() => [
{ style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap' }, { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap' },
actions.map((btn) => { actions.map((btn) => {
const isLoading = btn.label === '刷新' && refreshingSet.has(row.invoiceReqSerialNo) const isLoading = btn.label === '刷新' && refreshingSet.has(row.invoiceReqSerialNo)
const Icon = btn.icon
return h( return h(
NButton, NButton,
{ {
@@ -723,14 +840,19 @@ const columns = computed<DataTableColumn[]>(() => [
}, },
{ {
default: () => btn.label, 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 showDetail = ref(false)
const detailLoading = 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: 'lineNo', width: 60, align: 'center' },
{ title: '商品名称', key: 'goodsName', width: 140, ellipsis: { tooltip: true } }, { title: '商品名称', key: 'goodsName', width: 140, ellipsis: { tooltip: true } },
{ title: '税收分类编码', key: 'taxClassificationCode', width: 120, 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: '凭证类型', title: '凭证类型',
key: 'proofType', key: 'proofType',
@@ -1045,6 +1167,56 @@ onMounted(() => {
background: #fafafa !important; 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 { .detail-shell {
padding: 0 20px 20px; padding: 0 20px 20px;
} }