426 lines
11 KiB
Vue
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>
|