增加票通票样功能
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
log/
|
||||
|
||||
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.request.AskInvoiceRequest
|
||||
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.InvoiceDetailOrder
|
||||
import com.bbit.ticket.entity.response.InvoiceDetailResponse
|
||||
import com.bbit.ticket.entity.response.InvoiceDetailVoucher
|
||||
import com.bbit.ticket.entity.response.InvoiceHistoryItem
|
||||
import com.bbit.ticket.utils.Base64TextUtil
|
||||
import com.bbit.ticket.utils.formatDateTime
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
@@ -332,6 +334,7 @@ object BlueInvoiceDao {
|
||||
it.setIfNotNull(HistoryInvoiceBasicTable.definedData, req.definedData)
|
||||
// 文件
|
||||
it.setIfNotNull(HistoryInvoiceBasicTable.invoiceLayoutFileType, req.invoiceLayoutFileType)
|
||||
it.setIfNotNull(HistoryInvoiceBasicTable.downloadUrl, Base64TextUtil.decodeToText(req.downloadUrl))
|
||||
// 删除标记
|
||||
it[HistoryInvoiceBasicTable.invDeletedFlag] = req.invDeletedFlag ?: "0"
|
||||
}
|
||||
@@ -348,6 +351,18 @@ object BlueInvoiceDao {
|
||||
?: 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(),
|
||||
taxRateValue = row[HistoryInvoiceGoodsTable.taxRate].toPlainString(),
|
||||
taxRateAmount = row[HistoryInvoiceGoodsTable.taxRateAmount].toPlainString(),
|
||||
includeTaxFlag = false,
|
||||
includeTaxFlag = row[HistoryInvoiceGoodsTable.includeTaxFlag] == "1",
|
||||
zeroTaxFlag = row[HistoryInvoiceGoodsTable.zeroTaxFlag],
|
||||
preferentialPolicyFlag = row[HistoryInvoiceGoodsTable.preferentialPolicyFlag],
|
||||
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.HistoryInvoiceRedTable
|
||||
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
|
||||
import com.bbit.ticket.entity.response.InvoiceDownloadUrlResponse
|
||||
import com.bbit.ticket.entity.response.RedInvoiceInfoResponse
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
@@ -46,6 +47,7 @@ object RedInvoiceDao {
|
||||
it[HistoryInvoiceBasicTable.invoiceKind] = req.invoiceKind
|
||||
?: blueRow?.get(HistoryInvoiceBasicTable.invoiceKind) ?: "82"
|
||||
it[HistoryInvoiceBasicTable.invoiceType] = "2" // 红票
|
||||
it[HistoryInvoiceBasicTable.redFlag] = "REDING"
|
||||
|
||||
// ---- 红冲关联 ----
|
||||
it[HistoryInvoiceBasicTable.blueInvoiceCode] = blueRow?.get(HistoryInvoiceBasicTable.invoiceCode)
|
||||
@@ -115,4 +117,17 @@ object RedInvoiceDao {
|
||||
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:已删除
|
||||
*/
|
||||
val invDeletedFlag = varchar("inv_deleted_flag", 1)
|
||||
val downloadUrl = text("download_url").nullable()
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 时间字段
|
||||
|
||||
+5
-1
@@ -130,7 +130,11 @@ object HistoryInvoiceGoodsTable : Table("history_invoice_goods") {
|
||||
* 税额
|
||||
*/
|
||||
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.PTRedService
|
||||
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.response.header
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondBytes
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.post
|
||||
@@ -93,6 +97,86 @@ fun Route.registerPTiInvoiceRoutes() {
|
||||
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") {
|
||||
try {
|
||||
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.QueryInvoiceRequest
|
||||
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.InvoiceCreateResponse
|
||||
import com.bbit.ticket.entity.response.InvoiceDetailResponse
|
||||
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.net.PTClient
|
||||
import kotlin.uuid.Uuid
|
||||
@@ -70,6 +73,16 @@ object PTBlueService {
|
||||
suspend fun getInvoiceDetail(userId: Uuid, invoiceReqSerialNo: String): InvoiceDetailResponse? =
|
||||
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)
|
||||
*/
|
||||
@@ -81,4 +94,4 @@ object PTBlueService {
|
||||
return syncInvoiceFromPT(existing, invoiceReqSerialNo, req.taxpayerNum)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import com.bbit.ticket.dao.piaotong.HistoryDao
|
||||
import com.bbit.ticket.dao.piaotong.RedInvoiceDao
|
||||
import com.bbit.ticket.entity.request.QuickRedInvoiceRequest
|
||||
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.RedInvoiceInfoResponse
|
||||
import com.bbit.ticket.utils.CurrentUser
|
||||
import com.bbit.ticket.utils.plugins.dbQuery
|
||||
import com.bbit.ticket.utils.net.PTClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsBytes
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
@@ -59,4 +62,13 @@ object PTRedService {
|
||||
suspend fun getRedInvoiceInfo(userId: Uuid, invoiceReqSerialNo: String): RedInvoiceInfoResponse? =
|
||||
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 {
|
||||
|
||||
val isDev = false
|
||||
val isDev = true
|
||||
|
||||
// 请求基础地址
|
||||
var baseUrl: String
|
||||
|
||||
@@ -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