fix: 手动触发
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m15s

This commit is contained in:
sexygoat
2026-04-28 11:14:13 +08:00
parent b88f2d185e
commit fb10b72760
9 changed files with 462 additions and 205 deletions

View File

@@ -169,6 +169,7 @@ export interface AssetRealtimeStatusResponse {
real_name_status?: RealNameStatus // 实名状态(仅 card real_name_status?: RealNameStatus // 实名状态(仅 card
realname_policy?: string // 实名认证策略(仅 card realname_policy?: string // 实名认证策略(仅 card
current_month_usage_mb?: number // 本月已用流量 MB仅 card current_month_usage_mb?: number // 本月已用流量 MB仅 card
last_gateway_reading_mb?: number // 运营商周期月内已用流量 MB仅 card
last_sync_time?: string // 最后同步时间(仅 card last_sync_time?: string // 最后同步时间(仅 card
// ===== 设备专属字段 ===== // ===== 设备专属字段 =====

View File

@@ -7,6 +7,7 @@
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']

View File

@@ -33,7 +33,7 @@
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
<div v-if="hasCardInfo" class="operation-button-group"> <div v-if="hasCardInfo" class="operation-button-group">
<ElButton @click="handleRefresh" type="primary" :icon="Refresh"> 刷新 </ElButton> <ElButton @click="handleRefresh" type="primary" :icon="Refresh" :disabled="refreshDisabled"> 刷新 </ElButton>
</div> </div>
</div> </div>
</ElCard> </ElCard>
@@ -54,10 +54,12 @@
// Props // Props
interface Props { interface Props {
hasCardInfo?: boolean hasCardInfo?: boolean
refreshDisabled?: boolean
} }
withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
hasCardInfo: false hasCardInfo: false,
refreshDisabled: false
}) })
// Emits // Emits

View File

@@ -53,6 +53,9 @@
<ElDescriptionsItem label="本月已用流量">{{ <ElDescriptionsItem label="本月已用流量">{{
formatDataSize(cardInfo?.current_month_usage_mb || 0) formatDataSize(cardInfo?.current_month_usage_mb || 0)
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商周期月内已用流量">{{
formatDataSize(cardInfo?.last_gateway_reading_mb || 0)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="实名时间">{{ <ElDescriptionsItem label="实名时间">{{
cardInfo?.real_name_at ? formatDateTime(cardInfo.real_name_at) : '-' cardInfo?.real_name_at ? formatDateTime(cardInfo.real_name_at) : '-'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
@@ -480,6 +483,7 @@
real_name_status?: number real_name_status?: number
network_status?: number network_status?: number
current_month_usage_mb?: number current_month_usage_mb?: number
last_gateway_reading_mb?: number
device_type?: string device_type?: string
device_name?: string device_name?: string
device_model?: string device_model?: string

View File

@@ -4,7 +4,9 @@
<div class="card-header"> <div class="card-header">
<span>套餐列表</span> <span>套餐列表</span>
<div class="card-header-right"> <div class="card-header-right">
<ElButton type="primary" @click="handleShowRecharge">套餐充值</ElButton> <ElButton type="primary" :disabled="props.boundDeviceId" @click="handleShowRecharge"
>套餐充值</ElButton
>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -276,6 +276,9 @@ export function useAssetInfo() {
if (data.current_month_usage_mb !== undefined) { if (data.current_month_usage_mb !== undefined) {
cardInfo.value.current_month_usage_mb = data.current_month_usage_mb cardInfo.value.current_month_usage_mb = data.current_month_usage_mb
} }
if (data.last_gateway_reading_mb !== undefined) {
cardInfo.value.last_gateway_reading_mb = data.last_gateway_reading_mb
}
} else if (assetType === 'device') { } else if (assetType === 'device') {
// 设备的实时状态 // 设备的实时状态
if (data.device_protect_status !== undefined) { if (data.device_protect_status !== undefined) {
@@ -353,8 +356,9 @@ export function useAssetInfo() {
* 刷新资产数据(调用refresh接口后重新加载) * 刷新资产数据(调用refresh接口后重新加载)
* @param identifier - 资产标识符 * @param identifier - 资产标识符
* @param assetType - 资产类型(card/device) * @param assetType - 资产类型(card/device)
* @returns true if refresh was successful, false if rate limited (429)
*/ */
const refreshAsset = async (identifier: string, assetType: string) => { const refreshAsset = async (identifier: string, assetType: string): Promise<boolean> => {
try { try {
await AssetService.refreshAsset(identifier) await AssetService.refreshAsset(identifier)
ElMessage.success('刷新成功') ElMessage.success('刷新成功')
@@ -363,8 +367,15 @@ export function useAssetInfo() {
if (identifier) { if (identifier) {
await loadRealtimeStatus(identifier, assetType) await loadRealtimeStatus(identifier, assetType)
} }
return true
} catch (error: any) { } catch (error: any) {
console.log(error) console.log(error)
// Check if it's a 429 rate limit error
if (error?.response?.status === 429) {
ElMessage.warning('操作过于频繁请30秒后再试')
return false
}
return false
} }
} }

View File

@@ -4,6 +4,7 @@
<AssetSearchCard <AssetSearchCard
ref="assetSearchCardRef" ref="assetSearchCardRef"
:has-card-info="!!cardInfo" :has-card-info="!!cardInfo"
:refresh-disabled="refreshDisabled"
@search="handleSearch" @search="handleSearch"
@refresh="handleRefresh" @refresh="handleRefresh"
/> />
@@ -266,6 +267,9 @@
await updatePollingStatus(val) await updatePollingStatus(val)
}) })
// ========== 刷新按钮状态 ==========
const refreshDisabled = ref(false)
// ========== 事件处理 ========== // ========== 事件处理 ==========
/** /**
@@ -301,7 +305,14 @@
*/ */
const handleRefresh = async () => { const handleRefresh = async () => {
if (!cardInfo.value) return if (!cardInfo.value) return
await refreshAsset(cardInfo.value.identifier, cardInfo.value.asset_type) const success = await refreshAsset(cardInfo.value.identifier, cardInfo.value.asset_type)
if (!success) {
// 如果刷新失败429错误禁用刷新按钮30秒
refreshDisabled.value = true
setTimeout(() => {
refreshDisabled.value = false
}, 30000)
}
} }
/** /**

View File

@@ -78,7 +78,7 @@
v-model:filter="cardSearchForm" v-model:filter="cardSearchForm"
:items="cardSearchFormItems" :items="cardSearchFormItems"
label-width="120" label-width="120"
:show-expand="false" show-expand
@reset="handleCardSearchReset" @reset="handleCardSearchReset"
@search="handleCardSearch" @search="handleCardSearch"
></ArtSearchBar> ></ArtSearchBar>
@@ -107,6 +107,7 @@
:pageSize="cardPagination.pageSize" :pageSize="cardPagination.pageSize"
:total="cardPagination.total" :total="cardPagination.total"
:marginTop="10" :marginTop="10"
height="40vh"
@size-change="handleCardPageSizeChange" @size-change="handleCardPageSizeChange"
@current-change="handleCardPageChange" @current-change="handleCardPageChange"
@selection-change="handleAvailableCardsSelectionChange" @selection-change="handleAvailableCardsSelectionChange"
@@ -710,14 +711,14 @@
label: '自然月累计流量(MB)', label: '自然月累计流量(MB)',
width: 180, width: 180,
formatter: (row: EnterpriseCardItem) => formatter: (row: EnterpriseCardItem) =>
((row as any).current_month_usage_mb)?.toLocaleString() || '0' (row as any).current_month_usage_mb?.toLocaleString() || '0'
}, },
{ {
prop: 'last_month_total_mb', prop: 'last_month_total_mb',
label: '上月流量总量(MB)', label: '上月流量总量(MB)',
width: 150, width: 150,
formatter: (row: EnterpriseCardItem) => formatter: (row: EnterpriseCardItem) =>
((row as any).last_month_total_mb)?.toLocaleString() || '0' (row as any).last_month_total_mb?.toLocaleString() || '0'
}, },
{ {
prop: 'data_usage_mb', prop: 'data_usage_mb',
@@ -737,7 +738,9 @@
label: '最后实名检查时间', label: '最后实名检查时间',
width: 180, width: 180,
formatter: (row: EnterpriseCardItem) => formatter: (row: EnterpriseCardItem) =>
(row as any).last_real_name_check_at ? formatDateTime((row as any).last_real_name_check_at) : '-' (row as any).last_real_name_check_at
? formatDateTime((row as any).last_real_name_check_at)
: '-'
}, },
{ {
prop: 'enable_polling', prop: 'enable_polling',

View File

@@ -53,28 +53,28 @@
:pageSize="pagination.page_size" :pageSize="pagination.page_size"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"
:row-class-name="getRowClassName" :actions="getActions"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
> >
<template #default> <template #default>
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" /> <el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col">
<template v-if="col.slots?.default === 'progress'" #default="scope">
<div style="display: flex; align-items: center; gap: 8px">
<el-progress
:percentage="getProgressPercentage(scope.row)"
stroke-width="6"
:show-text="false"
style="flex: 1"
/>
<span style="font-size: 12px; width: 50px"
>{{ getProgressPercentage(scope.row) }}%</span
>
</div>
</template>
</el-table-column>
</template> </template>
</ArtTable> </ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</el-card> </el-card>
<!-- 批量触发弹窗 --> <!-- 批量触发弹窗 -->
@@ -85,91 +85,64 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@close="handleBatchDialogClose" @close="handleBatchDialogClose"
> >
<el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="100"> <el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="120">
<el-form-item label="任务类型" prop="task_type"> <div class="card-selection-wrapper">
<el-select <div class="card-search-bar">
v-model="batchForm.task_type" <ArtSearchBar
placeholder="请选择任务类型" v-model:filter="cardSearchForm"
style="width: 100%" :items="cardSearchFormItems"
> label-width="100"
<el-option label="实名认证轮询" value="polling:realname" /> show-expand
<el-option label="卡数据轮询" value="polling:carddata" /> @reset="handleCardSearchReset"
<el-option label="套餐轮询" value="polling:package" /> @search="handleCardSearch"
</el-select>
</el-form-item>
<el-form-item label="选择卡">
<div class="card-selection-wrapper">
<div class="card-search-bar">
<el-input
v-model="cardSearchKeyword"
placeholder="搜索ICCID/MSISDN"
clearable
style="width: 200px"
@input="handleCardSearchInput"
/>
<el-select
v-model="cardSearchForm.card_status"
placeholder="卡状态"
clearable
style="width: 150px; margin-left: 10px"
@change="handleCardSearch"
>
<el-option label="全部" value="" />
<el-option label="已激活" :value="1" />
<el-option label="未激活" :value="0" />
<el-option label="停机" :value="2" />
<el-option label="异常" :value="3" />
</el-select>
<el-button type="primary" style="margin-left: 10px" @click="handleCardSearch">
搜索
</el-button>
<el-button @click="handleCardSearchReset">重置</el-button>
</div>
<div class="card-table-info">
<span>已选择 {{ selectedBatchCards.length }} 张卡</span>
<span style="margin-left: 20px; color: #909399">
最多选择1000张卡
</span>
</div>
<el-table
ref="batchCardTableRef"
:data="batchCardList"
:loading="batchCardLoading"
row-key="id"
height="300"
@selection-change="handleBatchCardSelectionChange"
border
style="margin-top: 10px"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="iccid" label="ICCID" min-width="180" show-overflow-tooltip />
<el-table-column prop="msisdn" label="MSISDN" min-width="120" show-overflow-tooltip />
<el-table-column prop="carrier_name" label="运营商" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getCardStatusType(row.status)" size="small">
{{ getCardStatusName(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="batchCardPagination.page"
v-model:page-size="batchCardPagination.pageSize"
:total="batchCardPagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
style="margin-top: 10px"
small
@size-change="handleBatchCardPageSizeChange"
@current-change="handleBatchCardPageChange"
/> />
</div> </div>
</el-form-item> <div class="card-table-header">
<span>已选择 {{ selectedBatchCards.length }} 张卡</span>
<el-form-item
label="任务类型"
prop="task_type"
label-width="80"
style="margin-bottom: 0; width: 300px"
>
<el-select
v-model="batchForm.task_type"
placeholder="请选择任务类型"
style="width: 100%"
>
<el-option label="实名认证轮询" value="polling:realname" />
<el-option label="卡数据轮询" value="polling:carddata" />
<el-option label="套餐轮询" value="polling:package" />
</el-select>
</el-form-item>
</div>
<ArtTable
ref="batchCardTableRef"
row-key="id"
:loading="batchCardLoading"
:data="batchCardList"
:currentPage="batchCardPagination.page"
:pageSize="batchCardPagination.pageSize"
:total="batchCardPagination.total"
height="40vh"
@size-change="handleBatchCardPageSizeChange"
@current-change="handleBatchCardPageChange"
@selection-change="handleBatchCardSelectionChange"
>
<template #default>
<el-table-column type="selection" width="55" />
<el-table-column v-for="col in batchCardColumns" :key="col.prop" v-bind="col" />
</template>
</ArtTable>
</div>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="batchDialogVisible = false">取消</el-button> <el-button @click="batchDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleBatchSubmit" :disabled="selectedBatchCards.length === 0"> <el-button
type="primary"
@click="handleBatchSubmit"
:disabled="selectedBatchCards.length === 0"
>
确定 确定
</el-button> </el-button>
</template> </template>
@@ -351,8 +324,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue' import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus' import {
import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules' ElMessage,
ElMessageBox,
ElTag,
ElProgress,
type FormInstance,
type FormRules
} from 'element-plus'
import {
ManualTriggerService,
CarrierService,
ShopService,
CardService,
PackageSeriesService
} from '@/api/modules'
import type { import type {
ManualTriggerLog, ManualTriggerLog,
ManualTriggerHistoryQueryParams, ManualTriggerHistoryQueryParams,
@@ -362,15 +348,12 @@
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false) const loading = ref(false)
const tableData = ref<ManualTriggerLog[]>([]) const tableData = ref<ManualTriggerLog[]>([])
const tableRef = ref() const tableRef = ref()
const contextMenuRef = ref()
const currentClickRow = ref<ManualTriggerLog | null>(null)
const batchDialogVisible = ref(false) const batchDialogVisible = ref(false)
const conditionDialogVisible = ref(false) const conditionDialogVisible = ref(false)
@@ -409,16 +392,265 @@
pageSize: 20, pageSize: 20,
total: 0 total: 0
}) })
const cardSearchKeyword = ref('')
const cardSearchForm = reactive({
keyword: '',
card_status: undefined as number | undefined
})
const selectedBatchCards = ref<any[]>([]) const selectedBatchCards = ref<any[]>([])
const batchCardTableRef = ref() const batchCardTableRef = ref()
// 搜索防抖定时器 // 远程搜索相关
let cardSearchTimer: any = null const carrierLoading = ref(false)
const searchCarrierOptions = ref<any[]>([])
const searchSeriesOptions = ref<any[]>([])
const seriesLoading = ref(false)
// 卡搜索表单初始值
const initialCardSearchState = {
iccid: '',
carrier_name: '',
status: undefined,
virtual_no: '',
msisdn: '',
device_virtual_no: '',
series_id: undefined,
is_distributed: undefined,
is_replaced: undefined,
iccid_start: '',
iccid_end: ''
}
const cardSearchForm = reactive({ ...initialCardSearchState })
// 卡列表搜索表单配置
const cardSearchFormItems = computed<SearchFormItem[]>(() => [
{
label: 'ICCID',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID'
}
},
{
label: '运营商名称',
prop: 'carrier_name',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchCarrier,
loading: carrierLoading.value,
placeholder: '请选择或搜索运营商名称'
},
options: () =>
searchCarrierOptions.value.map((c) => ({
label: c.carrier_name,
value: c.carrier_name
}))
},
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '在库', value: 1 },
{ label: '已分销', value: 2 },
{ label: '已激活', value: 3 },
{ label: '已停用', value: 4 }
]
},
{
label: '卡虚拟号',
prop: 'virtual_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入卡虚拟号'
}
},
{
label: 'MSISDN',
prop: 'msisdn',
type: 'input',
config: {
clearable: true,
placeholder: '请输入MSISDN'
}
},
{
label: '绑定设备虚拟号',
prop: 'device_virtual_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入绑定设备虚拟号'
}
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeries,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '是否已分销',
prop: 'is_distributed',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: true },
{ label: '否', value: false }
]
},
{
label: '是否有换卡记录',
prop: 'is_replaced',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '有换卡记录', value: true },
{ label: '无换卡记录', value: false }
]
},
{
label: '是否绑定设备',
prop: 'is_standalone',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '未绑定设备', value: true },
{ label: '已绑定设备', value: false }
]
},
{
label: 'ICCID起始号',
prop: 'iccid_start',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID起始号'
}
},
{
label: 'ICCID结束号',
prop: 'iccid_end',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID结束号'
}
}
])
// 卡列表列配置
const batchCardColumns = computed(() => [
{
prop: 'iccid',
label: 'ICCID',
minWidth: 200
},
{
prop: 'msisdn',
label: 'MSISDN',
width: 160
},
{
prop: 'virtual_no',
label: '卡虚拟号',
width: 180,
showOverflowTooltip: true,
formatter: (row: any) => row.virtual_no || '-'
},
{
prop: 'device_virtual_no',
label: '绑定设备虚拟号',
width: 180,
showOverflowTooltip: true,
formatter: (row: any) => row.device_virtual_no || '-'
},
{
prop: 'card_category',
label: '卡业务类型',
width: 100,
formatter: (row: any) => getCardCategoryText(row.card_category)
},
{
prop: 'carrier_name',
label: '运营商',
width: 150,
showOverflowTooltip: true
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
formatter: (row: any) => row.shop_name || '-'
},
{
prop: 'series_name',
label: '套餐系列',
width: 150,
formatter: (row: any) => row.series_name || '-'
},
{
prop: 'status_name',
label: '状态',
width: 100
},
{
prop: 'activation_status',
label: '激活状态',
width: 100,
formatter: (row: any) => {
const type = row.activation_status === 1 ? 'success' : 'info'
const text = row.activation_status === 1 ? '已激活' : '未激活'
return h(ElTag, { type }, () => text)
}
},
{
prop: 'network_status',
label: '网络状态',
width: 100,
formatter: (row: any) => {
const type = row.network_status === 1 ? 'success' : 'danger'
const text = row.network_status === 1 ? '正常' : '停机'
return h(ElTag, { type }, () => text)
}
},
{
prop: 'real_name_status',
label: '实名状态',
width: 100,
formatter: (row: any) => {
const type = row.real_name_status === 1 ? 'success' : 'warning'
const text = row.real_name_status === 1 ? '已实名' : '未实名'
return h(ElTag, { type }, () => text)
}
}
])
// 获取批量触发弹窗中的卡列表 // 获取批量触发弹窗中的卡列表
const getBatchCardList = async () => { const getBatchCardList = async () => {
@@ -446,30 +678,50 @@
} }
} }
// 搜索输入(防抖) // 搜索运营商
const handleCardSearchInput = () => { const handleSearchCarrier = async (query: string) => {
if (cardSearchTimer) { try {
clearTimeout(cardSearchTimer) const params: any = { page: 1, page_size: 20 }
if (query) {
params.carrier_name = query
}
const res = await CarrierService.getCarriers(params)
if (res.code === 0) {
searchCarrierOptions.value = res.data.items || []
}
} catch (error) {
console.error(error)
}
}
// 搜索套餐系列
const handleSearchSeries = async (query: string) => {
seriesLoading.value = true
try {
const params: any = { page: 1, page_size: 20, status: 1 }
if (query) {
params.series_name = query
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error(error)
} finally {
seriesLoading.value = false
} }
cardSearchTimer = setTimeout(() => {
cardSearchForm.keyword = cardSearchKeyword.value
batchCardPagination.page = 1
getBatchCardList()
}, 300)
} }
// 卡搜索 // 卡搜索
const handleCardSearch = () => { const handleCardSearch = () => {
cardSearchForm.keyword = cardSearchKeyword.value
batchCardPagination.page = 1 batchCardPagination.page = 1
getBatchCardList() getBatchCardList()
} }
// 卡搜索重置 // 卡搜索重置
const handleCardSearchReset = () => { const handleCardSearchReset = () => {
cardSearchKeyword.value = '' Object.assign(cardSearchForm, { ...initialCardSearchState })
cardSearchForm.keyword = ''
cardSearchForm.card_status = undefined
batchCardPagination.page = 1 batchCardPagination.page = 1
getBatchCardList() getBatchCardList()
} }
@@ -500,9 +752,7 @@
// 关闭批量触发弹窗 // 关闭批量触发弹窗
const handleBatchDialogClose = () => { const handleBatchDialogClose = () => {
selectedBatchCards.value = [] selectedBatchCards.value = []
cardSearchKeyword.value = '' Object.assign(cardSearchForm, { ...initialCardSearchState })
cardSearchForm.keyword = ''
cardSearchForm.card_status = undefined
batchCardPagination.page = 1 batchCardPagination.page = 1
batchCardPagination.pageSize = 20 batchCardPagination.pageSize = 20
} }
@@ -529,6 +779,15 @@
return nameMap[status] || '未知' return nameMap[status] || '未知'
} }
// 获取卡业务类型文本
const getCardCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
normal: '普通卡',
industry: '行业卡'
}
return categoryMap[category] || category
}
// 条件触发表单 // 条件触发表单
const conditionForm = reactive<ConditionManualTriggerRequest>({ const conditionForm = reactive<ConditionManualTriggerRequest>({
task_type: '' as TaskType | '', task_type: '' as TaskType | '',
@@ -561,14 +820,6 @@
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }] task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
} }
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单配置 // 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [ const searchFormItems = computed<SearchFormItem[]>(() => [
{ {
@@ -615,29 +866,14 @@
minWidth: 120 minWidth: 120
}, },
{ {
prop: 'status', prop: 'status_name',
label: '状态', label: '状态',
minWidth: 100, minWidth: 100
slots: {
default: ({ row }: any) =>
h(ElTag, { type: getStatusTag(row.status) }, () => row.status_name)
}
}, },
{ {
label: '进度', label: '进度',
minWidth: 200, minWidth: 200,
slots: { slots: { default: 'progress' }
default: ({ row }: any) =>
h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
h('span', { style: 'font-size: 12px;' }, `${row.processed_count}/${row.total_count}`),
h('el-progress', {
percentage: getProgressPercentage(row),
'stroke-width': 6,
'show-text': false,
style: 'flex: 1;'
})
])
}
}, },
{ {
prop: 'success_count', prop: 'success_count',
@@ -671,23 +907,6 @@
})) }))
) )
const contextMenuItems = computed<MenuItemType[]>(() => {
const items: MenuItemType[] = [{ label: '刷新', key: 'refresh' }]
if (
currentClickRow.value &&
(currentClickRow.value.status === 'pending' || currentClickRow.value.status === 'processing')
) {
items.push({
label: '取消任务',
key: 'cancel',
permission: 'polling_manual_trigger_cancel'
})
}
return items
})
const loadData = async () => { const loadData = async () => {
loading.value = true loading.value = true
try { try {
@@ -719,6 +938,27 @@
loadData() loadData()
} }
const getActions = (row: ManualTriggerLog) => {
const actions: any[] = []
if (row.status === 'pending' || row.status === 'processing') {
if (hasAuth('polling_manual:trigger_cancel')) {
actions.push({
label: '取消任务',
type: 'warning',
handler: () => handleCancelTask(row)
})
}
}
if (hasAuth('polling_manual:refresh')) {
actions.push({
label: '刷新',
type: 'primary',
handler: () => handleRefresh()
})
}
return actions
}
const handleRefresh = () => { const handleRefresh = () => {
loadData() loadData()
} }
@@ -740,12 +980,12 @@
selectedBatchCards.value = [] selectedBatchCards.value = []
batchCardPagination.page = 1 batchCardPagination.page = 1
batchCardPagination.pageSize = 20 batchCardPagination.pageSize = 20
cardSearchKeyword.value = '' Object.assign(cardSearchForm, { ...initialCardSearchState })
cardSearchForm.keyword = ''
cardSearchForm.card_status = undefined
batchDialogVisible.value = true batchDialogVisible.value = true
nextTick(() => { nextTick(() => {
batchFormRef.value?.clearValidate() batchFormRef.value?.clearValidate()
handleSearchCarrier('')
handleSearchSeries('')
getBatchCardList() getBatchCardList()
}) })
} }
@@ -876,26 +1116,6 @@
} }
} }
const handleRowContextMenu = (row: ManualTriggerLog, _column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentClickRow.value = row
contextMenuRef.value?.show(event)
}
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentClickRow.value) return
switch (item.key) {
case 'refresh':
loadData()
break
case 'cancel':
handleCancelTask(currentClickRow.value)
break
}
}
// 加载运营商列表 // 加载运营商列表
const loadCarriers = async (carrierName?: string) => { const loadCarriers = async (carrierName?: string) => {
try { try {
@@ -965,12 +1185,14 @@
.card-selection-wrapper { .card-selection-wrapper {
.card-search-bar { .card-search-bar {
display: flex; display: block;
align-items: center;
} }
.card-table-info { .card-table-header {
margin-top: 10px; display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 0;
font-size: 14px; font-size: 14px;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }