fix: some
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Has been cancelled

This commit is contained in:
luo
2026-09-17 18:37:20 +08:00
parent 068d0c4ac2
commit 68d8f769d1
21 changed files with 1281 additions and 85 deletions

View File

@@ -0,0 +1,27 @@
import { BaseService } from '../BaseService'
import type {
BaseResponse,
AssetAutoRenewalConfig,
UpdateAssetAutoRenewalConfigRequest
} from '@/types/api'
/**
* 资产钱包相关服务
*/
export class AssetWalletService extends BaseService {
/**
* 查询自动续费配置
*/
static getAutoRenewalConfig(): Promise<BaseResponse<AssetAutoRenewalConfig>> {
return this.get<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config')
}
/**
* 保存自动续费配置
*/
static saveAutoRenewalConfig(
data: UpdateAssetAutoRenewalConfigRequest
): Promise<BaseResponse<AssetAutoRenewalConfig>> {
return this.put<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config', data)
}
}

View File

@@ -53,3 +53,7 @@ export { H5PopupConfigurationService } from './h5PopupConfiguration'
// export { SettingService } from './setting'
export { PackageTrafficAlertService } from './packageTrafficAlert'
export { PollingPriorityQueueService } from './pollingPriorityQueue'
export { AssetWalletService } from './assetWallet'

View File

@@ -0,0 +1,53 @@
/**
* 轮询优先队列 API
*/
import { BaseService } from '../BaseService'
import type {
BaseResponse,
PollingPriorityItem,
PollingPriorityItemPageResult,
PollingPriorityItemQueryParams,
CreatePollingPriorityItemRequest,
CreatePollingPriorityItemResponse
} from '@/types/api'
/**
* 轮询优先队列服务
*/
export class PollingPriorityQueueService extends BaseService {
/**
* 获取优先轮询项列表
* GET /api/admin/polling-priority-items
* 按创建时间倒序返回支持卡ID、任务类型、状态、触发类型筛选
*/
static getPriorityItems(
params?: PollingPriorityItemQueryParams
): Promise<BaseResponse<PollingPriorityItemPageResult>> {
return this.get<BaseResponse<PollingPriorityItemPageResult>>(
'/api/admin/polling-priority-items',
params
)
}
/**
* 人工优先入队
* POST /api/admin/polling-priority-items
* 请求体 { card_id, reason },为卡片全部范围内的轮询任务类型创建或合并优先项
*/
static createPriorityItems(
data: CreatePollingPriorityItemRequest
): Promise<BaseResponse<CreatePollingPriorityItemResponse>> {
return this.post<BaseResponse<CreatePollingPriorityItemResponse>>(
'/api/admin/polling-priority-items',
data
)
}
/**
* 获取优先轮询项详情
* GET /api/admin/polling-priority-items/{id}
*/
static getPriorityItemDetail(id: number): Promise<BaseResponse<PollingPriorityItem>> {
return this.get<BaseResponse<PollingPriorityItem>>(`/api/admin/polling-priority-items/${id}`)
}
}

View File

@@ -43,3 +43,6 @@ export * from './packageTrafficAlert'
export * from './bulkPurchase'
export * from './julyIteration'
export * from './augustIteration'
// 轮询优先队列相关
export * from './pollingPriorityQueue'

View File

@@ -0,0 +1,71 @@
/**
* 轮询优先队列相关常量
* 对应 docs/产品迭代8月份/优先队列.md
*/
import type {
PollingPriorityTaskType,
PollingPriorityItemStatus,
PollingPriorityTriggerType,
PollingPriorityResult
} from '@/types/api'
/** 任务类型选项 */
export const POLLING_PRIORITY_TASK_TYPE_OPTIONS: Array<{
label: string
value: PollingPriorityTaskType
}> = [
{ label: '实名检查', value: 'polling:realname' },
{ label: '流量检查', value: 'polling:carddata' },
{ label: '套餐检查', value: 'polling:package' },
{ label: '卡状态检查', value: 'polling:card_status' }
]
export const getPollingPriorityTaskTypeName = (
taskType?: PollingPriorityTaskType | string | null
) =>
POLLING_PRIORITY_TASK_TYPE_OPTIONS.find((item) => item.value === taskType)?.label ||
String(taskType || '-')
/** 状态选项 */
export const POLLING_PRIORITY_STATUS_OPTIONS: Array<{
label: string
value: PollingPriorityItemStatus
}> = [
{ label: '待处理', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '已完成', value: 'completed' },
{ label: '失败', value: 'failed' }
]
export const getPollingPriorityStatusName = (
status?: PollingPriorityItemStatus | string | null
) =>
POLLING_PRIORITY_STATUS_OPTIONS.find((item) => item.value === status)?.label ||
String(status || '-')
/** 触发类型选项 */
export const POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS: Array<{
label: string
value: PollingPriorityTriggerType
}> = [
{ label: '购买激活', value: 'purchase_activated' },
{ label: '续费激活', value: 'renewal_activated' },
{ label: '队列激活', value: 'queue_activated' },
{ label: '附加套餐激活', value: 'addon_activated' },
{ label: '无有效套餐', value: 'no_valid_package' },
{ label: '人工触发', value: 'manual_trigger' }
]
export const getPollingPriorityTriggerTypeName = (
triggerType?: PollingPriorityTriggerType | string | null
) =>
POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS.find((item) => item.value === triggerType)?.label ||
String(triggerType || '-')
/** 执行结果名称result 为空时展示为未出结果) */
export const getPollingPriorityResultName = (result?: PollingPriorityResult | string | null) => {
if (result === 'success') return '成功'
if (result === 'failed') return '失败'
return '未出结果'
}

View File

@@ -1216,6 +1216,16 @@ export const asyncRoutes: AppRouteRecord[] = [
title: '轮询监控',
keepAlive: true
}
},
// 优先队列
{
path: 'priority-queue',
name: 'PollingPriorityQueue',
component: RoutesAlias.PollingPriorityQueue,
meta: {
title: '优先队列',
keepAlive: true
}
}
]
}

View File

@@ -133,6 +133,7 @@ export enum RoutesAlias {
PollingConfig = '/polling-management/config', // 轮询配置
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
PollingMonitor = '/polling-management/monitor', // 轮询监控
PollingPriorityQueue = '/polling-management/priority-queue', // 优先队列
// 公共组件
Success = '/result/success', // 成功

View File

@@ -0,0 +1,46 @@
/**
* 资产钱包相关类型定义
*/
/**
* 自动续费适用范围
*/
export type AssetAutoRenewalScope = 'all' | 'specified'
/**
* 资产钱包自动续费配置对象
*/
export interface AssetAutoRenewalConfig {
/** 总开关1:开启0:关闭) */
enabled: 0 | 1
/** 总开关名称 */
enabled_name: string
/** 适用范围all:全部主套餐specified:指定主套餐) */
scope: AssetAutoRenewalScope
/** 适用范围名称 */
scope_name: string
/** 指定主套餐ID列表scope=specified 时非空) */
package_ids: number[]
/** 统一到期前天数1-90 */
days_before_expiry: number
/** 配置版本 */
config_version: number
/** 最近保存操作者 */
updater: string
/** 最近保存时间 */
updated_at: string
}
/**
* 更新资产钱包自动续费配置请求参数
*/
export interface UpdateAssetAutoRenewalConfigRequest {
/** 总开关0/1必填 */
enabled: 0 | 1
/** 适用范围all/specified必填 */
scope: AssetAutoRenewalScope
/** 指定主套餐ID列表scope=specified 时必填非空且仅当前可售主套餐) */
package_ids: number[]
/** 统一到期前天数1-90必填 */
days_before_expiry: number
}

View File

@@ -147,3 +147,9 @@ export * from './h5PopupConfiguration'
// 套餐真流量预警相关
export * from './packageTrafficAlert'
// 轮询优先队列相关
export * from './pollingPriorityQueue'
// 资产钱包自动续费相关
export * from './assetWallet'

View File

@@ -0,0 +1,171 @@
/**
* 轮询优先队列相关类型定义
*/
/**
* 优先轮询任务类型
*/
export type PollingPriorityTaskType =
| 'polling:realname'
| 'polling:carddata'
| 'polling:package'
| 'polling:card_status'
/**
* 优先轮询项状态
*/
export type PollingPriorityItemStatus = 'pending' | 'processing' | 'completed' | 'failed'
/**
* 优先轮询项触发类型
*/
export type PollingPriorityTriggerType =
| 'purchase_activated'
| 'renewal_activated'
| 'queue_activated'
| 'addon_activated'
| 'no_valid_package'
| 'manual_trigger'
/**
* 优先轮询执行结果
*/
export type PollingPriorityResult = 'success' | 'failed' | ''
/**
* 优先轮询项对象
*/
export interface PollingPriorityItem {
/** 优先轮询项ID */
id: number
/** 卡ID */
card_id: number
/** 卡ICCID */
iccid: string
/** 任务类型 */
task_type: PollingPriorityTaskType
/** 任务类型名称 */
task_type_name: string
/** 状态 */
status: PollingPriorityItemStatus
/** 状态名称 */
status_name: string
/** 触发类型 */
trigger_type: PollingPriorityTriggerType
/** 触发类型名称 */
trigger_type_name: string
/** 触发类型列表 */
trigger_types: PollingPriorityTriggerType[]
/** 触发次数 */
trigger_count: number
/** 尝试次数 */
attempt_count: number
/** 执行结果 */
result: PollingPriorityResult
/** 执行结果名称 */
result_name: string
/** 失败原因 */
failure_reason: string | null
/** 店铺快照ID */
shop_id_snapshot: number | null
/** 来源订单ID */
source_order_id: number | null
/** 来源套餐使用记录ID */
source_package_usage_id: number | null
/** 人工操作者ID */
manual_operator_id: number | null
/** 人工操作者名称 */
manual_operator_name: string | null
/** 人工入队原因 */
manual_reason: string | null
/** 认领时间 */
claimed_at: string | null
/** 最后触发时间 */
last_triggered_at: string | null
/** 创建时间 */
created_at: string
/** 更新时间 */
updated_at: string
}
/**
* 优先轮询项查询参数
*/
export interface PollingPriorityItemQueryParams {
/** 页码默认1最小1 */
page?: number
/** 每页数量默认20最大100 */
page_size?: number
/** 卡ID筛选 */
card_id?: number
/** 任务类型筛选 */
task_type?: PollingPriorityTaskType
/** 状态筛选 */
status?: PollingPriorityItemStatus
/** 触发类型筛选 */
trigger_type?: PollingPriorityTriggerType
}
/**
* 优先轮询项分页响应
*/
export interface PollingPriorityItemPageResult {
/** 优先轮询项列表 */
items: PollingPriorityItem[]
/** 当前页 */
page: number
/** 每页数量 */
size: number
/** 总数 */
total: number
}
/**
* 人工优先入队请求参数
*/
export interface CreatePollingPriorityItemRequest {
/** 卡ID */
card_id: number
/** 入队原因必填最长500字符 */
reason: string
}
/**
* 人工入队结果项
*/
export interface PriorityItemResult {
/** 优先轮询项ID */
id: number
/** 卡ID */
card_id: number
/** 卡ICCID */
iccid: string
/** 任务类型 */
task_type: PollingPriorityTaskType
/** 任务类型名称 */
task_type_name: string
/** 状态 */
status: PollingPriorityItemStatus
/** 状态名称 */
status_name: string
/** 触发次数 */
trigger_count: number
/** 创建时间 */
created_at: string
}
/**
* 人工优先入队响应
*/
export interface CreatePollingPriorityItemResponse {
/** 新建数量 */
created_count: number
/** 合并数量 */
merged_count: number
/** 涉及任务类型列表 */
task_types: PollingPriorityTaskType[]
/** 涉及任务类型名称列表 */
task_type_names: string[]
/** 入队结果项列表 */
items: PriorityItemResult[]
}

View File

@@ -4,7 +4,6 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="110"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"

View File

@@ -4,7 +4,6 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="110"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"

View File

@@ -300,7 +300,7 @@
<ElFormItem label="主体类型" prop="subject_type">
<ElRadioGroup v-model="qualificationForm.subject_type">
<ElRadio label="enterprise">企业</ElRadio>
<ElRadio label="individual">个人</ElRadio>
<ElRadio label="personal">个人</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="主体证件号" prop="subject_code">
@@ -367,11 +367,7 @@
</ElFormItem>
<div class="qualification-row">
<ElFormItem label="发票抬头">
<ElInput
v-model="qualificationForm.invoice_title"
placeholder="选填"
maxlength="100"
/>
<ElInput v-model="qualificationForm.invoice_title" placeholder="选填" maxlength="100" />
</ElFormItem>
<ElFormItem label="发票主体证件号">
<ElInput
@@ -399,18 +395,28 @@
<div v-loading="detailLoading" class="withdrawal-detail">
<template v-if="withdrawalDetail">
<ElDescriptions :column="1" border>
<ElDescriptionsItem label="提现单号">{{ withdrawalDetail.withdrawal_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="提现单号">{{
withdrawalDetail.withdrawal_no
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">{{ withdrawalDetail.status_name }}</ElDescriptionsItem>
<ElDescriptionsItem label="提现金额">{{ formatMoney(withdrawalDetail.amount) }}</ElDescriptionsItem>
<ElDescriptionsItem label="实际到账">{{ formatMoney(withdrawalDetail.actual_amount) }}</ElDescriptionsItem>
<ElDescriptionsItem label="手续费">{{ formatMoney(withdrawalDetail.fee) }}</ElDescriptionsItem>
<ElDescriptionsItem label="提现金额">{{
formatMoney(withdrawalDetail.amount)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="实际到账">{{
formatMoney(withdrawalDetail.actual_amount)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="手续费">{{
formatMoney(withdrawalDetail.fee)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="提现方式">
{{ withdrawalMethodLabel(withdrawalDetail.withdrawal_method) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="收款账户">
{{ withdrawalDetail.account_name }} / {{ withdrawalDetail.account_number }}
</ElDescriptionsItem>
<ElDescriptionsItem label="驳回原因">{{ withdrawalDetail.reject_reason || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="驳回原因">{{
withdrawalDetail.reject_reason || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="异常标记">
<ElTag :type="withdrawalDetail.anomaly_flag ? 'danger' : 'success'">
{{ withdrawalDetail.anomaly_name }}
@@ -442,12 +448,7 @@
</ElDrawer>
<!-- 重新提交被驳回的提现 -->
<ElDialog
v-model="resubmitDialogVisible"
title="重新提交提现"
width="560px"
destroy-on-close
>
<ElDialog v-model="resubmitDialogVisible" title="重新提交提现" width="560px" destroy-on-close>
<ElForm
ref="resubmitFormRef"
:model="resubmitForm"
@@ -503,11 +504,7 @@
</ElForm>
<template #footer>
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
:loading="resubmitSubmitting"
@click="handleSubmitResubmit"
>
<ElButton type="primary" :loading="resubmitSubmitting" @click="handleSubmitResubmit">
提交
</ElButton>
</template>
@@ -529,7 +526,6 @@
confirm-permission="commission_record:export"
title="导出佣金记录"
/>
</div>
</template>
@@ -549,7 +545,7 @@
WithdrawalQualificationSubmitParams,
WithdrawalRequestDetail,
ResubmitWithdrawalRequestParams,
ShopCommissionRecordSource,
ShopCommissionRecordSource
} from '@/types/api/commission'
import { WithdrawalMethod, WithdrawalStatus } from '@/types/api/commission'
import type { Order, OrderQueryParams } from '@/types/api/order'
@@ -1336,7 +1332,6 @@
}
})
// ==================== 提现资料资格 ====================
const qualificationLoading = ref(false)
@@ -1371,54 +1366,52 @@
return labels.length > 0 ? labels.join('、') : '-'
}
const {
columnChecks: qualificationColumnChecks,
columns: qualificationColumns
} = useCheckedColumns(() => [
{ prop: 'subject_type_name', label: '主体类型', width: 90 },
{ prop: 'subject_code_masked', label: '主体证件号', minWidth: 160 },
{ prop: 'legal_person_id_card_masked', label: '法人身份证', minWidth: 170 },
{
prop: 'status_name',
label: '状态',
width: 100,
formatter: (row: WithdrawalQualificationItem) => {
const tagType = row.status === 0 ? 'warning' : row.status === 1 ? 'success' : 'danger'
return h(ElTag, { type: tagType }, () => row.status_name || String(row.status))
const { columnChecks: qualificationColumnChecks, columns: qualificationColumns } =
useCheckedColumns(() => [
{ prop: 'subject_type_name', label: '主体类型', width: 90 },
{ prop: 'subject_code_masked', label: '主体证件号', minWidth: 160 },
{ prop: 'legal_person_id_card_masked', label: '法人身份证', minWidth: 170 },
{
prop: 'status_name',
label: '状态',
width: 100,
formatter: (row: WithdrawalQualificationItem) => {
const tagType = row.status === 0 ? 'warning' : row.status === 1 ? 'success' : 'danger'
return h(ElTag, { type: tagType }, () => row.status_name || String(row.status))
}
},
{
prop: 'approval_status_name',
label: '审批状态',
width: 100,
formatter: (row: WithdrawalQualificationItem) => row.approval_status_name || '-'
},
{
prop: 'attachments',
label: '附件',
minWidth: 200,
formatter: (row: WithdrawalQualificationItem) => qualificationAttachmentLabels(row)
},
{
prop: 'invalid_reason',
label: '作废原因',
minWidth: 160,
formatter: (row: WithdrawalQualificationItem) => row.invalid_reason || '-'
},
{
prop: 'invalidated_at',
label: '作废时间',
width: 170,
formatter: (row: WithdrawalQualificationItem) =>
row.invalidated_at ? formatDateTime(row.invalidated_at) : '-'
},
{
prop: 'created_at',
label: '创建时间',
width: 170,
formatter: (row: WithdrawalQualificationItem) => formatDateTime(row.created_at)
}
},
{
prop: 'approval_status_name',
label: '审批状态',
width: 100,
formatter: (row: WithdrawalQualificationItem) => row.approval_status_name || '-'
},
{
prop: 'attachments',
label: '附件',
minWidth: 200,
formatter: (row: WithdrawalQualificationItem) => qualificationAttachmentLabels(row)
},
{
prop: 'invalid_reason',
label: '作废原因',
minWidth: 160,
formatter: (row: WithdrawalQualificationItem) => row.invalid_reason || '-'
},
{
prop: 'invalidated_at',
label: '作废时间',
width: 170,
formatter: (row: WithdrawalQualificationItem) =>
row.invalidated_at ? formatDateTime(row.invalidated_at) : '-'
},
{
prop: 'created_at',
label: '创建时间',
width: 170,
formatter: (row: WithdrawalQualificationItem) => formatDateTime(row.created_at)
}
])
])
const getQualificationList = async () => {
if (!currentShopId.value) return
@@ -1562,8 +1555,7 @@
confirmButtonText: '确认作废',
cancelButtonText: '取消',
inputPlaceholder: '作废原因(必填)',
inputValidator: (reason: string) =>
reason && reason.trim() ? true : '作废原因必填',
inputValidator: (reason: string) => (reason && reason.trim() ? true : '作废原因必填'),
type: 'warning'
})
await CommissionService.voidWithdrawalQualification(row.id, { reason: value.trim() })
@@ -1571,8 +1563,8 @@
getQualificationList()
} catch {
// 取消或请求失败
}
}
}
}
// ==================== 提现详情与重提 ====================
@@ -1591,10 +1583,7 @@
detailLoading.value = true
withdrawalDetail.value = null
try {
const res = await CommissionService.getWithdrawalRequestDetail(
currentShopId.value,
row.id
)
const res = await CommissionService.getWithdrawalRequestDetail(currentShopId.value, row.id)
if (res.code === 0) {
withdrawalDetail.value = res.data
}
@@ -1606,7 +1595,11 @@
}
const getWithdrawalActions = (row: WithdrawalRequestItem) => {
const actions: Array<{ label: string; handler: (row: any) => void; type?: 'primary' | 'danger' }> = [
const actions: Array<{
label: string
handler: (row: any) => void
type?: 'primary' | 'danger'
}> = [
{
label: '详情',
handler: () => openWithdrawalDetail(row),
@@ -1755,7 +1748,6 @@
</script>
<style scoped lang="scss">
.qualification-row {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@@ -273,7 +273,7 @@
]
}
if (isActing.value) {
base.acting_reason = [{ required: true, message: '请填写代办原因', trigger: 'blur' }]
base.acting_reason = [{ required: false, message: '请填写代办原因', trigger: 'blur' }]
}
return base
})

View File

@@ -0,0 +1,492 @@
<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"
>
<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>
</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,
slots: {
default: ({ row }: any) =>
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,
slots: {
default: ({ row }: any) =>
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>