Files
Ticket/web/src/features/statistics/openapi/index.vue
T

426 lines
11 KiB
Vue

<template>
<div class="page-shell">
<section class="page-card table-fill">
<div class="page-toolbar">
<h2 class="page-toolbar-title">OpenAPI</h2>
<div class="page-toolbar-actions">
<n-button :loading="loading" @click="load">
<template #icon><n-icon :component="RefreshCw" /></template>
刷新
</n-button>
</div>
</div>
<div class="queue-summary">
<div class="metric">
<span>数电账号</span>
<strong>{{ rows.length }}</strong>
</div>
<div class="metric">
<span>待处理</span>
<strong>{{ totals.pending }}</strong>
</div>
<div class="metric">
<span>处理中</span>
<strong>{{ totals.processing }}</strong>
</div>
<div class="metric">
<span>需认证</span>
<strong>{{ totals.waitingAuth }}</strong>
</div>
</div>
<div class="card-body card-body-fill table-fill">
<n-data-table
flex-height
size="small"
:columns="columns"
:data="rows"
:loading="loading"
:pagination="{ pageSize: 12 }"
:row-key="(row: OpenInvoiceTaskOverviewItem) => row.digitalAccountId"
:row-props="rowProps"
:scroll-x="1160"
/>
</div>
</section>
<n-drawer v-model:show="drawerVisible" :width="980">
<n-drawer-content :title="drawerTitle" closable>
<div class="drawer-toolbar">
<n-select
v-model:value="taskStatus"
clearable
size="small"
placeholder="全部状态"
:options="statusOptions"
class="task-filter"
@update:value="loadTasks(1)"
/>
<n-select
v-model:value="taskSourceType"
clearable
size="small"
placeholder="全部来源"
:options="sourceTypeOptions"
class="task-filter"
@update:value="loadTasks(1)"
/>
<n-button size="small" :loading="taskLoading" @click="loadTasks(taskPage)">
<template #icon><n-icon :component="RefreshCw" /></template>
刷新
</n-button>
</div>
<n-data-table
size="small"
:columns="taskColumns"
:data="tasks"
:loading="taskLoading"
:pagination="taskPagination"
:scroll-x="1280"
remote
@update:page="loadTasks"
@update:page-size="changeTaskPageSize"
/>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script setup lang="ts">
import { computed, h, onMounted, reactive, ref } from 'vue'
import type { DataTableColumns, PaginationProps } from 'naive-ui'
import { NButton, NDataTable, NDrawer, NDrawerContent, NIcon, NSelect, NTag } from 'naive-ui'
import { ListTree, Pause, Play, RefreshCw } from 'lucide-vue-next'
import {
openInvoiceTaskOverviewApi,
openInvoiceTaskPageApi,
pauseOpenInvoiceTaskQueueApi,
resumeOpenInvoiceTaskQueueApi
} from '@/api/piaotong'
import type { OpenInvoiceTaskItem, OpenInvoiceTaskOverviewItem } from '@/api/piaotong'
import { appMessage } from '@/utils/message'
const loading = ref(false)
const rows = ref<OpenInvoiceTaskOverviewItem[]>([])
const selected = ref<OpenInvoiceTaskOverviewItem | null>(null)
const drawerVisible = ref(false)
const taskLoading = ref(false)
const tasks = ref<OpenInvoiceTaskItem[]>([])
const taskPage = ref(1)
const taskPageSize = ref(20)
const taskTotal = ref(0)
const taskStatus = ref<string | null>(null)
const taskSourceType = ref<string | null>(null)
const statusOptions = [
{ label: '待处理', value: 'PENDING' },
{ label: '处理中', value: 'PROCESSING' },
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAILED' },
{ label: '需认证', value: 'WAITING_AUTH' }
]
const sourceTypeOptions = [
{ label: '单笔', value: 'SINGLE' },
{ label: '批量', value: 'BATCH' }
]
const queueStatusLabelMap: Record<string, string> = {
RUNNING: '运行中',
PAUSED: '已暂停'
}
const taskStatusLabelMap: Record<string, string> = {
PENDING: '待处理',
PROCESSING: '处理中',
SUCCESS: '成功',
FAILED: '失败',
WAITING_AUTH: '需认证'
}
const taskTypeLabelMap: Record<string, string> = {
ISSUE_BLUE: '蓝票开具',
QUERY_BLUE: '蓝票查询'
}
const sourceTypeLabelMap: Record<string, string> = {
SINGLE: '单笔',
BATCH: '批量'
}
function labelOf(map: Record<string, string>, value?: string | null) {
return value ? map[value] || value : '-'
}
const totals = computed(() =>
rows.value.reduce(
(acc, row) => {
acc.pending += row.pending
acc.processing += row.processing
acc.waitingAuth += row.waitingAuth
return acc
},
{ pending: 0, processing: 0, waitingAuth: 0 }
)
)
const drawerTitle = computed(() => {
const row = selected.value
if (!row) return '任务明细'
return `${row.account || row.digitalAccountId} 任务明细`
})
const statusTagType = (status: string) => {
if (status === 'RUNNING' || status === 'SUCCESS') return 'success'
if (status === 'PAUSED' || status === 'WAITING_AUTH') return 'warning'
if (status === 'FAILED') return 'error'
if (status === 'PROCESSING') return 'info'
return 'default'
}
const columns: DataTableColumns<OpenInvoiceTaskOverviewItem> = [
{ title: '数电账号', key: 'account', minWidth: 160, render: (row) => row.account || '-' },
{ title: '数电账号ID', key: 'digitalAccountId', minWidth: 220 },
{
title: '队列',
key: 'status',
width: 110,
render: (row) =>
h(
NTag,
{ type: statusTagType(row.status), size: 'small' },
{ default: () => labelOf(queueStatusLabelMap, row.status) }
)
},
{ title: '待处理', key: 'pending', width: 90 },
{ title: '处理中', key: 'processing', width: 90 },
{
title: '需认证',
key: 'waitingAuth',
width: 90,
render: (row) =>
h(
NTag,
{ type: row.waitingAuth > 0 ? 'warning' : 'default', size: 'small' },
{ default: () => row.waitingAuth }
)
},
{ title: '成功', key: 'success', width: 90 },
{
title: '失败',
key: 'failed',
width: 90,
render: (row) =>
h(
NTag,
{ type: row.failed > 0 ? 'error' : 'default', size: 'small' },
{ default: () => row.failed }
)
},
{ title: '总数', key: 'total', width: 90 },
{ title: '最近任务', key: 'lastCreatedAt', minWidth: 150 },
{
title: '操作',
key: 'actions',
width: 168,
align: 'center',
render: (row) =>
h(
'div',
{ class: 'action-buttons' },
[
h(
NButton,
{
size: 'small',
secondary: true,
title: '查看任务详情',
onClick: (event: MouseEvent) => {
event.stopPropagation()
openDrawer(row)
}
},
{
icon: () => h(NIcon, { component: ListTree }),
default: () => '详情'
}
),
h(
NButton,
{
size: 'small',
secondary: true,
type: row.status === 'PAUSED' ? 'success' : 'warning',
title: row.status === 'PAUSED' ? '恢复队列' : '暂停队列',
onClick: (event: MouseEvent) => {
event.stopPropagation()
row.status === 'PAUSED' ? resume(row) : pause(row)
}
},
{
icon: () => h(NIcon, { component: row.status === 'PAUSED' ? Play : Pause }),
default: () => (row.status === 'PAUSED' ? '恢复' : '暂停')
}
)
]
)
}
]
function rowProps(row: OpenInvoiceTaskOverviewItem) {
return {
style: 'cursor: pointer;',
onClick: () => {
openDrawer(row)
}
}
}
const taskColumns: DataTableColumns<OpenInvoiceTaskItem> = [
{ title: '票号', key: 'invoiceReqSerialNo', minWidth: 170 },
{ title: '任务', key: 'taskType', width: 120, render: (row) => labelOf(taskTypeLabelMap, row.taskType) },
{ title: '来源', key: 'sourceType', width: 90, render: (row) => labelOf(sourceTypeLabelMap, row.sourceType) },
{
title: '状态',
key: 'status',
width: 110,
render: (row) =>
h(
NTag,
{ type: statusTagType(row.status), size: 'small' },
{ default: () => labelOf(taskStatusLabelMap, row.status) }
)
},
{ title: 'PT码', key: 'ptCode', width: 90 },
{ title: '错误', key: 'errorMessage', minWidth: 260, ellipsis: { tooltip: true } },
{ title: '查询次数', key: 'pollCount', width: 90, render: (row) => `${row.pollCount}/${row.maxPollCount}` },
{ title: '重试', key: 'attemptCount', width: 90, render: (row) => `${row.attemptCount}/${row.maxAttemptCount}` },
{ title: '下次执行', key: 'nextRunAt', minWidth: 150 }
]
const taskPagination = reactive<PaginationProps>({
page: taskPage.value,
pageSize: taskPageSize.value,
itemCount: taskTotal.value,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
async function load() {
loading.value = true
try {
rows.value = await openInvoiceTaskOverviewApi()
} finally {
loading.value = false
}
}
async function loadTasks(page = 1) {
if (!selected.value) return
taskLoading.value = true
try {
const result = await openInvoiceTaskPageApi({
digitalAccountId: selected.value.digitalAccountId,
status: taskStatus.value,
sourceType: taskSourceType.value,
page,
pageSize: taskPageSize.value
})
tasks.value = result.items
taskPage.value = result.page
taskTotal.value = result.total
taskPagination.page = result.page
taskPagination.itemCount = result.total
} finally {
taskLoading.value = false
}
}
function openDrawer(row: OpenInvoiceTaskOverviewItem) {
selected.value = row
drawerVisible.value = true
taskStatus.value = null
taskSourceType.value = null
loadTasks(1)
}
async function changeTaskPageSize(pageSize: number) {
taskPageSize.value = pageSize
taskPagination.pageSize = pageSize
await loadTasks(1)
}
async function pause(row: OpenInvoiceTaskOverviewItem) {
await pauseOpenInvoiceTaskQueueApi(row.digitalAccountId, '手动暂停')
appMessage.success('队列已暂停')
await load()
}
async function resume(row: OpenInvoiceTaskOverviewItem) {
await resumeOpenInvoiceTaskQueueApi(row.digitalAccountId)
appMessage.success('队列已恢复')
await load()
if (selected.value?.digitalAccountId === row.digitalAccountId) {
await loadTasks(taskPage.value)
}
}
onMounted(load)
</script>
<style scoped>
.queue-summary {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 12px;
padding: 0 16px 12px;
}
.metric {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
background: var(--card-color);
}
.metric span {
display: block;
color: var(--text-color-2);
font-size: 12px;
}
.metric strong {
display: block;
margin-top: 4px;
font-size: 22px;
font-weight: 700;
}
.drawer-toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 12px;
}
.task-filter {
width: 140px;
}
.action-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 144px;
}
@media (max-width: 720px) {
.queue-summary {
grid-template-columns: repeat(2, minmax(120px, 1fr));
}
}
</style>