Files
one-pipe-system/src/views/polling-management/priority-queue/index.vue
luo ac541b17af
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m35s
fix: some
2026-09-17 18:43:01 +08:00

493 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ArtTableFullScreen>
<div class="priority-queue-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
/>
<el-card shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<el-button type="primary" @click="showCreateDialog">人工优先入队</el-button>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="100"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</el-card>
</div>
<!-- 人工优先入队 -->
<el-dialog
v-model="createDialogVisible"
title="人工优先入队"
width="520px"
:close-on-click-modal="false"
>
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<el-form-item label="卡ICCID" prop="card_id">
<el-select
v-model="createForm.card_id"
placeholder="请输入ICCID搜索"
filterable
remote
:remote-method="remoteSearchCard"
clearable
no-data-text="暂无卡数据请输入ICCID搜索"
style="width: 100%"
>
<el-option
v-for="card in cardList"
:key="card.id"
:label="card.iccid"
:value="card.id"
/>
</el-select>
</el-form-item>
<el-form-item label="入队原因" prop="reason">
<el-input
v-model="createForm.reason"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="请输入入队原因最多500字"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="createLoading" @click="handleCreateSubmit">
确定
</el-button>
</template>
</el-dialog>
<!-- 入队结果 -->
<el-dialog v-model="resultDialogVisible" title="入队结果" width="620px">
<div class="result-summary">
<el-tag type="success" class="result-item">新建 {{ createResult?.created_count ?? 0 }} </el-tag>
<el-tag type="warning" class="result-item">合并 {{ createResult?.merged_count ?? 0 }} </el-tag>
<span class="result-item">
任务类型{{ (createResult?.task_type_names || []).join('、') || '-' }}
</span>
</div>
<el-table :data="createResult?.items || []" border stripe size="small" max-height="360">
<el-table-column prop="iccid" label="卡ICCID" min-width="160" show-overflow-tooltip />
<el-table-column prop="task_type_name" label="任务类型" min-width="120" />
<el-table-column prop="status_name" label="状态" width="100" />
<el-table-column prop="trigger_count" label="触发次数" width="100" />
</el-table>
</el-dialog>
<!-- 优先轮询项详情 -->
<el-dialog
v-model="detailDialogVisible"
title="优先轮询项详情"
width="640px"
:close-on-click-modal="false"
>
<div v-loading="detailLoading">
<el-descriptions v-if="detailData" :column="2" border>
<el-descriptions-item label="ID">{{ detailData.id }}</el-descriptions-item>
<el-descriptions-item label="卡ICCID">{{ detailData.iccid }}</el-descriptions-item>
<el-descriptions-item label="任务类型">
{{ detailData.task_type_name }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(detailData.status)">{{
detailData.status_name
}}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="触发类型">
{{ detailData.trigger_type_name }}
</el-descriptions-item>
<el-descriptions-item label="触发次数">{{ detailData.trigger_count }}</el-descriptions-item>
<el-descriptions-item label="尝试次数">{{ detailData.attempt_count }}</el-descriptions-item>
<el-descriptions-item label="执行结果">
{{ detailData.result_name }}
</el-descriptions-item>
<el-descriptions-item label="失败原因" :span="2">
{{ detailData.failure_reason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="人工操作者">
{{ detailData.manual_operator_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="入队原因">
{{ detailData.manual_reason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="认领时间">
{{ formatDateTime(detailData.claimed_at) }}
</el-descriptions-item>
<el-descriptions-item label="最后触发时间">
{{ formatDateTime(detailData.last_triggered_at) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(detailData.created_at) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(detailData.updated_at) }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h } from 'vue'
import { ElMessage, ElTag, type FormInstance, type FormRules } from 'element-plus'
import { PollingPriorityQueueService, CardService } from '@/api/modules'
import type {
PollingPriorityItem,
PollingPriorityItemQueryParams,
CreatePollingPriorityItemResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import {
POLLING_PRIORITY_TASK_TYPE_OPTIONS,
POLLING_PRIORITY_STATUS_OPTIONS,
POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS,
getPollingPriorityResultName
} from '@/config/constants'
defineOptions({ name: 'PollingPriorityQueue' })
const loading = ref(false)
const tableData = ref<PollingPriorityItem[]>([])
const tableRef = ref()
// 分页数据
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 搜索表单
const searchForm = reactive({
card_id: undefined as number | undefined,
task_type: undefined as string | undefined,
status: undefined as string | undefined,
trigger_type: undefined as string | undefined
})
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '卡ID',
prop: 'card_id',
type: 'input',
config: { clearable: true, placeholder: '请输入卡ID' }
},
{
label: '任务类型',
prop: 'task_type',
type: 'select',
options: POLLING_PRIORITY_TASK_TYPE_OPTIONS,
config: { clearable: true, placeholder: '请选择任务类型' }
},
{
label: '状态',
prop: 'status',
type: 'select',
options: POLLING_PRIORITY_STATUS_OPTIONS,
config: { clearable: true, placeholder: '请选择状态' }
},
{
label: '触发类型',
prop: 'trigger_type',
type: 'select',
options: POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS,
config: { clearable: true, placeholder: '请选择触发类型' }
}
]
const getStatusTagType = (status: string) => {
const types: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
pending: 'info',
processing: 'warning',
completed: 'success',
failed: 'danger'
}
return types[status] || 'info'
}
const getResultTagType = (result: string) => {
if (result === 'success') return 'success'
if (result === 'failed') return 'danger'
return 'info'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'iccid',
label: '卡ICCID',
minWidth: 170,
showOverflowTooltip: true
},
{
prop: 'task_type_name',
label: '任务类型',
width: 130,
formatter: (row: PollingPriorityItem) => row.task_type_name
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: PollingPriorityItem) =>
h(ElTag, { type: getStatusTagType(row.status) }, () => row.status_name)
},
{
prop: 'trigger_type_name',
label: '触发类型',
width: 130,
formatter: (row: PollingPriorityItem) => row.trigger_type_name
},
{
prop: 'trigger_count',
label: '触发次数',
width: 100
},
{
prop: 'attempt_count',
label: '尝试次数',
width: 100
},
{
prop: 'result',
label: '执行结果',
width: 110,
formatter: (row: PollingPriorityItem) =>
h(
ElTag,
{ type: getResultTagType(row.result) },
() => getPollingPriorityResultName(row.result)
)
},
{
prop: 'manual_operator_name',
label: '操作者',
width: 120,
formatter: (row: PollingPriorityItem) => row.manual_operator_name || '-'
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: PollingPriorityItem) => formatDateTime(row.created_at)
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
const getActions = () => [{ label: '详情', handler: showDetail, type: 'primary' as const }]
const loadData = async () => {
loading.value = true
try {
const queryParams: PollingPriorityItemQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
card_id: searchForm.card_id,
task_type: searchForm.task_type as never,
status: searchForm.status as never,
trigger_type: searchForm.trigger_type as never
}
const { data } = await PollingPriorityQueueService.getPriorityItems(queryParams)
tableData.value = data.items || []
pagination.total = data.total || 0
} catch (error) {
console.error('加载优先轮询项列表失败', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
loadData()
}
const handleReset = () => {
Object.assign(searchForm, {
card_id: undefined,
task_type: undefined,
status: undefined,
trigger_type: undefined
})
pagination.page = 1
loadData()
}
const handleRefresh = () => {
loadData()
}
const handleSizeChange = (size: number) => {
pagination.page_size = size
loadData()
}
const handleCurrentChange = (page: number) => {
pagination.page = page
loadData()
}
// 人工优先入队
const createDialogVisible = ref(false)
const createLoading = ref(false)
const createFormRef = ref<FormInstance>()
const createResult = ref<CreatePollingPriorityItemResponse | null>(null)
const resultDialogVisible = ref(false)
const cardList = ref<Array<{ id: number; iccid: string }>>([])
const createForm = reactive({
card_id: undefined as number | undefined,
reason: ''
})
const createRules: FormRules = {
card_id: [{ required: true, message: '请选择卡', trigger: 'change' }],
reason: [
{ required: true, message: '请输入入队原因', trigger: 'blur' },
{ max: 500, message: '入队原因不能超过500个字符', trigger: 'blur' }
]
}
const showCreateDialog = () => {
createForm.card_id = undefined
createForm.reason = ''
createResult.value = null
createDialogVisible.value = true
cardList.value = []
loadCards()
}
const loadCards = async (iccid?: string) => {
try {
const { data } = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20,
iccid
})
cardList.value = data.items || []
} catch (error) {
console.error('加载卡列表失败', error)
}
}
const remoteSearchCard = (query: string) => {
loadCards(query)
}
const handleCreateSubmit = async () => {
if (!createFormRef.value) return
const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
if (createForm.card_id === undefined) {
ElMessage.warning('请选择卡')
return
}
createLoading.value = true
try {
const { data } = await PollingPriorityQueueService.createPriorityItems({
card_id: createForm.card_id,
reason: createForm.reason.trim()
})
createResult.value = data
createDialogVisible.value = false
resultDialogVisible.value = true
await loadData()
} catch (error) {
console.error('人工优先入队失败', error)
} finally {
createLoading.value = false
}
}
// 详情
const detailDialogVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<PollingPriorityItem | null>(null)
const showDetail = async (row: PollingPriorityItem) => {
detailData.value = null
detailDialogVisible.value = true
detailLoading.value = true
try {
const { data } = await PollingPriorityQueueService.getPriorityItemDetail(row.id)
detailData.value = data
} catch (error) {
console.error('加载优先轮询项详情失败', error)
} finally {
detailLoading.value = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.priority-queue-page {
display: flex;
flex-direction: column;
height: 100%;
.result-summary {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.result-item {
font-size: 13px;
}
}
}
</style>