Files
one-pipe-system/src/views/asset-management/iot-card-management/index.vue
sexygoat b510b4539f
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 11m14s
修复bug
2026-04-08 10:19:25 +08:00

1781 lines
55 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="standalone-card-list-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
label-width="90"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton
type="success"
:disabled="selectedCards.length === 0"
@click="showAllocateDialog"
v-permission="'iot_card:batch_allocation'"
>
批量分配
</ElButton>
<ElButton
type="warning"
:disabled="selectedCards.length === 0"
@click="showRecallDialog"
v-permission="'iot_card:batch_recycle'"
>
批量回收
</ElButton>
<ElButton
type="primary"
:disabled="selectedCards.length === 0"
@click="showSeriesBindingDialog"
v-permission="'iot_card:batch_setting'"
>
批量设置套餐系列
</ElButton>
<ElButton
v-if="
hasAuth('iot_card:batch_recharge') ||
hasAuth('iot_card:network_distribution') ||
hasAuth('iot_card:network_recycle') ||
hasAuth('iot_card:change_package') ||
hasAuth('iot_card:batch_download')
"
type="info"
@click="showMoreMenuOnClick"
>
更多操作
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="cardList"
:currentPage="pagination.page"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
>
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 批量分配对话框 -->
<ElDialog
v-model="allocateDialogVisible"
title="批量分配"
width="600px"
@close="handleAllocateDialogClose"
>
<ElForm
ref="allocateFormRef"
:model="allocateForm"
:rules="allocateRules"
label-width="120px"
>
<ElFormItem label="目标店铺" prop="to_shop_id">
<ElSelect
v-model="allocateForm.to_shop_id"
placeholder="请选择或搜索目标店铺"
filterable
remote
reserve-keyword
:remote-method="searchTargetShops"
:loading="targetShopLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="shop in targetShopList"
:key="shop.id"
:label="shop.shop_name"
:value="shop.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="allocateForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="allocateForm.carrier_id"
placeholder="请选择运营商"
clearable
style="width: 100%"
>
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="allocateForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="批次号"
>
<ElInput v-model="allocateForm.batch_no" placeholder="请输入批次号" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput
v-model="allocateForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="allocateDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
确认分配
</ElButton>
</div>
</template>
</ElDialog>
<!-- 批量回收对话框 -->
<ElDialog
v-model="recallDialogVisible"
title="批量回收"
width="600px"
@close="handleRecallDialogClose"
>
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="recallForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="recallForm.carrier_id"
placeholder="请选择运营商"
clearable
style="width: 100%"
>
<ElOption label="中国移动" :value="1" />
<ElOption label="中国联通" :value="2" />
<ElOption label="中国电信" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.FILTER"
label="批次号"
>
<ElInput v-model="recallForm.batch_no" placeholder="请输入批次号" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput
v-model="recallForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="recallDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleRecall" :loading="recallLoading">
确认回收
</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配结果对话框 -->
<ElDialog v-model="resultDialogVisible" :title="resultTitle" width="700px">
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="操作单号">{{
allocationResult.allocation_no
}}</ElDescriptionsItem>
<ElDescriptionsItem label="待处理总数">{{
allocationResult.total_count
}}</ElDescriptionsItem>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ allocationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ allocationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div
v-if="allocationResult.failed_items && allocationResult.failed_items.length > 0"
style="margin-top: 20px"
>
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="allocationResult.failed_items" border max-height="300">
<ElTableColumn prop="iccid" label="ICCID" width="200" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="resultDialogVisible = false">确定</ElButton>
</div>
</template>
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog
v-model="seriesBindingDialogVisible"
title="批量设置套餐系列绑定"
width="600px"
@close="handleSeriesBindingDialogClose"
>
<ElForm
ref="seriesBindingFormRef"
:model="seriesBindingForm"
:rules="seriesBindingRules"
label-width="120px"
>
<ElFormItem label="已选择卡数">
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem label="套餐系列" prop="series_id">
<ElSelect
v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列"
style="width: 100%"
filterable
remote
reserve-keyword
:remote-method="searchPackageSeries"
:loading="seriesLoading"
clearable
>
<ElOption label="清除关联" :value="0" />
<ElOption
v-for="series in packageSeriesList"
:key="series.id"
:label="series.series_name"
:value="series.id"
:disabled="series.status !== 1"
/>
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="seriesBindingDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSeriesBinding" :loading="seriesBindingLoading">
确认设置
</ElButton>
</div>
</template>
</ElDialog>
<!-- 套餐系列绑定结果对话框 -->
<ElDialog
v-model="seriesBindingResultDialogVisible"
title="设置结果"
width="700px"
@close="handleSeriesBindingResultDialogClose"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ seriesBindingResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div
v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0"
style="margin-top: 20px"
>
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="seriesBindingResult.failed_items" border max-height="300">
<ElTableColumn prop="iccid" label="ICCID" width="200" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false"
>确定</ElButton
>
</div>
</template>
</ElDialog>
<!-- 卡详情对话框 -->
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
<div v-if="cardDetailLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载中...</div>
</div>
<ElDescriptions v-else-if="currentCardDetail" :column="3" border>
<ElDescriptionsItem label="卡ID">{{ currentCardDetail.id }}</ElDescriptionsItem>
<ElDescriptionsItem label="ICCID" :span="2">{{
currentCardDetail.iccid
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡接入号">{{
currentCardDetail.msisdn || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商">{{
currentCardDetail.carrier_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商类型">{{
getCarrierTypeText(currentCardDetail.carrier_type)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡业务类型">{{
getCardCategoryText(currentCardDetail.card_category)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getCardDetailStatusTagType(currentCardDetail.status)">
{{ getCardDetailStatusText(currentCardDetail.status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐系列">{{
currentCardDetail.series_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="激活状态">
<ElTag :type="currentCardDetail.activation_status === 1 ? 'success' : 'info'">
{{ currentCardDetail.activation_status === 1 ? '已激活' : '未激活' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag :type="currentCardDetail.real_name_status === 1 ? 'success' : 'warning'">
{{ currentCardDetail.real_name_status === 1 ? '已实名' : '未实名' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="网络状态">
<ElTag :type="currentCardDetail.network_status === 1 ? 'success' : 'danger'">
{{ currentCardDetail.network_status === 1 ? '正常' : '停机' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="累计流量使用"
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
>
<ElDescriptionsItem label="一次性佣金">
<ElTag :type="currentCardDetail.first_commission_paid ? 'success' : 'info'">
{{ currentCardDetail.first_commission_paid ? '已产生' : '未产生' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="累计充值">{{
formatCardPrice(currentCardDetail.accumulated_recharge)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(currentCardDetail.created_at)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">{{
formatDateTime(currentCardDetail.updated_at)
}}</ElDescriptionsItem>
</ElDescriptions>
<ElEmpty v-else description="未找到卡片信息" />
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="cardDetailDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 流量使用查询对话框 -->
<ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px">
<div v-if="flowUsageLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
<ElDescriptions v-else-if="flowUsageData" :column="1" border>
<ElDescriptionsItem label="套餐总流量"
>{{ flowUsageData.totalFlow || 0 }} MB</ElDescriptionsItem
>
<ElDescriptionsItem label="已用流量"
>{{ flowUsageData.usedFlow || 0 }} MB</ElDescriptionsItem
>
<ElDescriptionsItem label="剩余流量"
>{{ flowUsageData.remainFlow || 0 }} MB</ElDescriptionsItem
>
<ElDescriptionsItem v-if="flowUsageData.extend" label="扩展信息">{{
flowUsageData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="flowUsageDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 实名状态查询对话框 -->
<ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px">
<div v-if="realnameStatusLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
<ElDescriptions v-else-if="realnameStatusData" :column="1" border>
<ElDescriptionsItem label="实名状态">{{
realnameStatusData.status || '未知'
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="realnameStatusData.extend" label="扩展信息">{{
realnameStatusData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="realnameStatusDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 卡实时状态查询对话框 -->
<ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px">
<div v-if="cardStatusLoading" style="padding: 40px; text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div>
</div>
<ElDescriptions v-else-if="cardStatusData" :column="1" border>
<ElDescriptionsItem label="ICCID">{{
cardStatusData.iccid || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="卡状态">{{
cardStatusData.cardStatus || '未知'
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="cardStatusData.extend" label="扩展信息">{{
cardStatusData.extend
}}</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="cardStatusDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 更多操作右键菜单 -->
<ArtMenuRight
ref="moreMenuRef"
:menu-items="moreMenuItems"
:menu-width="180"
@select="handleMoreMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { CardService, ShopService, PackageSeriesService, AssetService } from '@/api/modules'
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import type {
StandaloneIotCard,
StandaloneCardStatus,
AllocateStandaloneCardsRequest,
RecallStandaloneCardsRequest,
AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import { CardSelectionType } from '@/types/api/card'
import type { PackageSeriesResponse } from '@/types/api'
defineOptions({ name: 'StandaloneCardList' })
const { hasAuth } = useAuth()
const router = useRouter()
const loading = ref(false)
const allocateDialogVisible = ref(false)
const allocateLoading = ref(false)
const recallDialogVisible = ref(false)
const recallLoading = ref(false)
const resultDialogVisible = ref(false)
const resultTitle = ref('')
const tableRef = ref()
const allocateFormRef = ref<FormInstance>()
const recallFormRef = ref<FormInstance>()
const selectedCards = ref<StandaloneIotCard[]>([])
const allocationResult = ref<AllocateStandaloneCardsResponse>({
allocation_no: '',
total_count: 0,
success_count: 0,
fail_count: 0,
failed_items: null
})
// 套餐系列绑定相关
const seriesBindingDialogVisible = ref(false)
const seriesBindingLoading = ref(false)
const seriesBindingResultDialogVisible = ref(false)
const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false)
const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({
series_id: undefined as number | undefined
})
const seriesBindingRules = reactive<FormRules>({
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }]
})
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
success_count: 0,
fail_count: 0,
failed_items: null
})
// 卡详情弹窗相关
const cardDetailDialogVisible = ref(false)
const cardDetailLoading = ref(false)
const currentCardDetail = ref<any>(null)
// IoT卡操作相关对话框
const flowUsageDialogVisible = ref(false)
const flowUsageLoading = ref(false)
const flowUsageData = ref<any>(null)
const realnameStatusDialogVisible = ref(false)
const realnameStatusLoading = ref(false)
const realnameStatusData = ref<any>(null)
const cardStatusDialogVisible = ref(false)
const cardStatusLoading = ref(false)
const cardStatusData = ref<any>(null)
// 更多操作右键菜单
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
// 店铺相关
const targetShopLoading = ref(false)
const targetShopList = ref<any[]>([])
// 搜索表单初始值
const initialSearchState: {
status: undefined | number
carrier_id: undefined | number
iccid: string
msisdn: string
virtual_no: string
is_distributed: undefined | number
[key: string]: any
} = {
status: undefined,
carrier_id: undefined,
iccid: '',
msisdn: '',
virtual_no: '',
is_distributed: undefined
}
// 搜索表单
const formFilters = reactive({ ...initialSearchState })
// 批量分配表单
const allocateForm = reactive<Partial<AllocateStandaloneCardsRequest>>({
selection_type: CardSelectionType.LIST,
to_shop_id: undefined,
iccids: [],
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
status: undefined,
batch_no: '',
remark: ''
})
// 批量分配表单验证规则
const allocateRules = reactive<FormRules>({
to_shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (allocateForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (allocateForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 批量回收表单
const recallForm = reactive<Partial<RecallStandaloneCardsRequest>>({
selection_type: CardSelectionType.LIST,
iccids: [],
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
batch_no: '',
remark: ''
})
// 批量回收表单验证规则
const recallRules = reactive<FormRules>({
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (recallForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (recallForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 搜索表单配置
const formItems: SearchFormItem[] = [
{
label: '状态',
prop: 'status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '在库', value: 1 },
{ label: '已分销', value: 2 },
{ label: '已激活', value: 3 },
{ label: '已停用', value: 4 }
]
},
{
label: '运营商',
prop: 'carrier_id',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '中国移动', value: 1 },
{ label: '中国联通', value: 2 },
{ label: '中国电信', value: 3 }
]
},
{
label: 'ICCID',
prop: 'iccid',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID'
}
},
{
label: '卡接入号',
prop: 'msisdn',
type: 'input',
config: {
clearable: true,
placeholder: '请输入卡接入号'
}
},
{
label: '虚拟号',
prop: 'virtual_no',
type: 'input',
config: {
clearable: true,
placeholder: '请输入虚拟号'
}
},
{
label: '是否已分销',
prop: 'is_distributed',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
]
// 列配置
const columnOptions = [
{ label: 'ICCID', prop: 'iccid' },
{ label: '卡接入号', prop: 'msisdn' },
{ label: '虚拟号', prop: 'virtual_no' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '状态', prop: 'status' },
{ label: '激活状态', prop: 'activation_status' },
{ label: '网络状态', prop: 'network_status' },
{ label: '实名状态', prop: 'real_name_status' },
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
{ label: '一次性佣金', prop: 'first_commission_paid' },
{ label: '累计充值', prop: 'accumulated_recharge' },
{ label: '创建时间', prop: 'created_at' }
]
const cardList = ref<StandaloneIotCard[]>([])
// 获取状态标签类型
const getStatusType = (status: StandaloneCardStatus) => {
switch (status) {
case 1:
return 'info'
case 2:
return 'warning'
case 3:
return 'success'
case 4:
return 'danger'
default:
return 'info'
}
}
// 获取状态文本
const getStatusText = (status: StandaloneCardStatus) => {
switch (status) {
case 1:
return '在库'
case 2:
return '已分销'
case 3:
return '已激活'
case 4:
return '已停用'
default:
return '未知'
}
}
// 跳转到资产信息页面(原IoT卡详情)
const goToCardDetail = (iccid: string) => {
if (hasAuth('iot_card:view_detail')) {
router.push({
path: '/asset-management/asset-information',
query: {
iccid: iccid
}
})
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 卡详情辅助函数
const getCarrierTypeText = (type: string) => {
const typeMap: Record<string, string> = {
CMCC: '中国移动',
CUCC: '中国联通',
CTCC: '中国电信',
CBN: '中国广电'
}
return typeMap[type] || type
}
const getCardCategoryText = (category: string) => {
const categoryMap: Record<string, string> = {
normal: '普通卡',
industry: '行业卡'
}
return categoryMap[category] || category
}
const getCardDetailStatusText = (status: number) => {
const statusMap: Record<number, string> = {
1: '在库',
2: '已分销',
3: '已激活',
4: '已停用'
}
return statusMap[status] || '未知'
}
const getCardDetailStatusTagType = (status: number) => {
const typeMap: Record<number, any> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return typeMap[status] || 'info'
}
const formatCardPrice = (price: number) => {
return `¥${(price / 100).toFixed(2)}`
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'iccid',
label: 'ICCID',
minWidth: 200,
formatter: (row: StandaloneIotCard) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
goToCardDetail(row.iccid)
}
},
row.iccid
)
}
},
{
prop: 'msisdn',
label: '卡接入号',
width: 160
},
{
prop: 'virtual_no',
label: '虚拟号',
width: 160,
formatter: (row: StandaloneIotCard) => row.virtual_no
},
{
prop: 'card_category',
label: '卡业务类型',
width: 100,
formatter: (row: StandaloneIotCard) => getCardCategoryText(row.card_category)
},
{
prop: 'carrier_name',
label: '运营商',
width: 150
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
formatter: (row: StandaloneIotCard) => row.shop_name || '-'
},
{
prop: 'series_name',
label: '套餐系列',
width: 150,
formatter: (row: StandaloneIotCard) => row.series_name || '-'
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: StandaloneIotCard) => {
return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status))
}
},
{
prop: 'activation_status',
label: '激活状态',
width: 100,
formatter: (row: StandaloneIotCard) => {
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: StandaloneIotCard) => {
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: StandaloneIotCard) => {
const type = row.real_name_status === 1 ? 'success' : 'warning'
const text = row.real_name_status === 1 ? '已实名' : '未实名'
return h(ElTag, { type }, () => text)
}
},
{
prop: 'data_usage_mb',
label: '累计流量(MB)',
width: 120
},
{
prop: 'first_commission_paid',
label: '一次性佣金',
width: 100,
formatter: (row: StandaloneIotCard) => {
const type = row.first_commission_paid ? 'success' : 'info'
const text = row.first_commission_paid ? '已产生' : '未产生'
return h(ElTag, { type, size: 'small' }, () => text)
}
},
{
prop: 'accumulated_recharge',
label: '累计充值',
width: 100,
formatter: (row: StandaloneIotCard) => `¥${(row.accumulated_recharge / 100).toFixed(2)}`
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
}
])
// 操作列配置
const getActions = (row: StandaloneIotCard) => {
const actions: any[] = []
// 查询流量按钮
if (hasAuth('iot_card:query_flow')) {
actions.push({
label: '查询流量',
handler: () => showFlowUsageDialog(row.iccid),
type: 'primary'
})
}
// 其他操作放到更多中
if (hasAuth('iot_card:query_realname_status')) {
actions.push({
label: '查询实名状态',
handler: () => handleCardOperation('realname-status', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:query_card_status')) {
actions.push({
label: '查询卡状态',
handler: () => handleCardOperation('card-status', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:start_card')) {
actions.push({
label: '启用此卡',
handler: () => handleCardOperation('start-card', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:stop_card')) {
actions.push({
label: '停用此卡',
handler: () => handleCardOperation('stop-card', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:manual_deactivate')) {
actions.push({
label: '手动停用',
handler: () => handleCardOperation('manual-deactivate', row.iccid),
type: 'primary'
})
}
return actions
}
onMounted(() => {
getTableData()
})
// 获取单卡列表
const getTableData = async () => {
loading.value = true
try {
const params: Record<string, any> = {
page: pagination.page,
page_size: pagination.pageSize,
...formFilters
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key]
}
})
const res = await CardService.getStandaloneIotCards(params)
if (res.code === 0) {
cardList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 表格选择变化
const handleSelectionChange = (selection: StandaloneIotCard[]) => {
selectedCards.value = selection
}
// 加载目标店铺列表
const loadTargetShops = async (shopName?: string) => {
targetShopLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
if (shopName) {
params.shop_name = shopName
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
targetShopList.value = res.data.items || []
}
} catch (error) {
console.error('获取目标店铺列表失败:', error)
} finally {
targetShopLoading.value = false
}
}
// 搜索目标店铺
const searchTargetShops = async (query: string) => {
await loadTargetShops(query || undefined)
}
// 显示批量分配对话框
const showAllocateDialog = () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要分配的卡')
return
}
allocateDialogVisible.value = true
Object.assign(allocateForm, {
selection_type: CardSelectionType.LIST,
to_shop_id: undefined,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
status: undefined,
batch_no: '',
remark: ''
})
// 加载默认店铺列表
loadTargetShops()
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
}
// 显示批量回收对话框
const showRecallDialog = () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要回收的卡')
return
}
recallDialogVisible.value = true
Object.assign(recallForm, {
selection_type: CardSelectionType.LIST,
iccids: selectedCards.value.map((card) => card.iccid),
iccid_start: '',
iccid_end: '',
carrier_id: undefined,
batch_no: '',
remark: ''
})
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
}
// 关闭批量分配对话框
const handleAllocateDialogClose = () => {
if (allocateFormRef.value) {
allocateFormRef.value.resetFields()
}
}
// 关闭批量回收对话框
const handleRecallDialogClose = () => {
if (recallFormRef.value) {
recallFormRef.value.resetFields()
}
}
// 执行批量分配
const handleAllocate = async () => {
if (!allocateFormRef.value) return
await allocateFormRef.value.validate(async (valid) => {
if (valid) {
// 根据选卡方式构建请求参数
const params: Partial<AllocateStandaloneCardsRequest> = {
selection_type: allocateForm.selection_type!,
to_shop_id: allocateForm.to_shop_id!,
remark: allocateForm.remark
}
if (allocateForm.selection_type === CardSelectionType.LIST) {
params.iccids = selectedCards.value.map((card) => card.iccid)
if (params.iccids.length === 0) {
ElMessage.warning('请先选择要分配的卡')
return
}
} else if (allocateForm.selection_type === CardSelectionType.RANGE) {
params.iccid_start = allocateForm.iccid_start
params.iccid_end = allocateForm.iccid_end
} else if (allocateForm.selection_type === CardSelectionType.FILTER) {
if (allocateForm.carrier_id) params.carrier_id = allocateForm.carrier_id
if (allocateForm.status) params.status = allocateForm.status
if (allocateForm.batch_no) params.batch_no = allocateForm.batch_no
}
allocateLoading.value = true
try {
const res = await CardService.allocateStandaloneCards(params)
// code === 0 表示操作成功(接口调用成功),显示结果对话框
if (res.code === 0) {
allocationResult.value = res.data
resultTitle.value = '批量分配结果'
allocateDialogVisible.value = false
resultDialogVisible.value = true
// 清空选择
selectedCards.value = []
// 刷新列表
await getTableData()
}
} catch (error) {
console.error(error)
} finally {
allocateLoading.value = false
}
}
})
}
// 执行批量回收
const handleRecall = async () => {
if (!recallFormRef.value) return
await recallFormRef.value.validate(async (valid) => {
if (valid) {
// 根据选卡方式构建请求参数
const params: Partial<RecallStandaloneCardsRequest> = {
selection_type: recallForm.selection_type!,
remark: recallForm.remark
}
if (recallForm.selection_type === CardSelectionType.LIST) {
params.iccids = selectedCards.value.map((card) => card.iccid)
if (params.iccids.length === 0) {
ElMessage.warning('请先选择要回收的卡')
return
}
} else if (recallForm.selection_type === CardSelectionType.RANGE) {
params.iccid_start = recallForm.iccid_start
params.iccid_end = recallForm.iccid_end
} else if (recallForm.selection_type === CardSelectionType.FILTER) {
if (recallForm.carrier_id) params.carrier_id = recallForm.carrier_id
if (recallForm.batch_no) params.batch_no = recallForm.batch_no
}
recallLoading.value = true
try {
const res = await CardService.recallStandaloneCards(params)
// code === 0 表示操作成功(接口调用成功),显示结果对话框
if (res.code === 0) {
allocationResult.value = res.data
resultTitle.value = '批量回收结果'
recallDialogVisible.value = false
resultDialogVisible.value = true
// 清空选择
selectedCards.value = []
// 刷新列表
await getTableData()
} else {
// code !== 0 才是真正失败(接口调用失败)
ElMessage.error(res.msg || '批量回收失败,请重试')
}
} catch (error) {
console.error(error)
} finally {
recallLoading.value = false
}
}
})
}
// 显示套餐系列绑定对话框
const showSeriesBindingDialog = async () => {
if (selectedCards.value.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
// 加载套餐系列列表
await loadPackageSeriesList()
seriesBindingDialogVisible.value = true
seriesBindingForm.series_id = undefined
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
// 加载套餐系列列表支持名称搜索默认20条
const loadPackageSeriesList = async (seriesName?: string) => {
seriesLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20,
status: 1 // 只获取启用的
}
if (seriesName) {
params.series_name = seriesName
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0 && res.data.items) {
packageSeriesList.value = res.data.items
}
} catch (error) {
console.error('获取套餐系列列表失败:', error)
} finally {
seriesLoading.value = false
}
}
// 搜索套餐系列
const searchPackageSeries = async (query: string) => {
await loadPackageSeriesList(query || undefined)
}
// 关闭套餐系列绑定对话框
const handleSeriesBindingDialogClose = () => {
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
// 关闭套餐系列绑定结果对话框
const handleSeriesBindingResultDialogClose = () => {
// 刷新列表
getTableData()
}
// 执行套餐系列绑定
const handleSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return
await seriesBindingFormRef.value.validate(async (valid) => {
if (valid) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
seriesBindingLoading.value = true
try {
const res = await CardService.batchSetCardSeriesBinding({
iccids,
series_id: seriesBindingForm.series_id!
})
if (res.code === 0) {
seriesBindingResult.value = res.data
seriesBindingDialogVisible.value = false
seriesBindingResultDialogVisible.value = true
// 清空选择
selectedCards.value = []
// 立即刷新列表
await getTableData()
// 显示消息提示
if (res.data.fail_count === 0) {
ElMessage.success('套餐系列绑定设置成功')
} else if (res.data.success_count === 0) {
ElMessage.error('套餐系列绑定设置失败')
} else {
ElMessage.warning(
`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count} 项`
)
}
}
} catch (error) {
console.error(error)
} finally {
seriesBindingLoading.value = false
}
}
})
}
// 更多操作菜单项配置
const moreMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('iot_card:batch_download')) {
items.push({
key: 'download',
label: '批量下载'
})
}
if (hasAuth('iot_card:change_package')) {
items.push({
key: 'changePackage',
label: '其他功能暂定'
})
}
return items
})
// 卡操作菜单项配置
// 显示更多操作菜单 (左键点击)
const showMoreMenuOnClick = (e: MouseEvent) => {
e.stopPropagation()
moreMenuRef.value?.show(e)
}
// 处理更多操作菜单选择
const handleMoreMenuSelect = (item: MenuItemType) => {
switch (item.key) {
case 'distribution':
cardDistribution()
break
case 'recharge':
batchRecharge()
break
case 'recycle':
cardRecycle()
break
case 'download':
batchDownload()
break
case 'changePackage':
changePackage()
break
}
}
// 网卡分销 - 正在开发中
const cardDistribution = () => {
ElMessage.info('功能正在开发中')
}
// 批量充值 - 正在开发中
const batchRecharge = () => {
ElMessage.info('功能正在开发中')
}
// 网卡回收 - 正在开发中
const cardRecycle = () => {
ElMessage.info('功能正在开发中')
}
// 批量下载 - 正在开发中
const batchDownload = () => {
ElMessage.info('功能正在开发中')
}
// 变更套餐 - 正在开发中
const changePackage = () => {
ElMessage.info('功能正在开发中')
}
// IoT卡操作处理函数
const handleCardOperation = (command: string, iccid: string) => {
switch (command) {
case 'realname-status':
showRealnameStatusDialog(iccid)
break
case 'card-status':
showCardStatusDialog(iccid)
break
case 'start-card':
handleStartCard(iccid)
break
case 'stop-card':
handleStopCard(iccid)
break
case 'manual-deactivate':
handleManualDeactivate(iccid)
break
}
}
// 查询流量使用
const showFlowUsageDialog = async (iccid: string) => {
flowUsageDialogVisible.value = true
flowUsageLoading.value = true
flowUsageData.value = null
try {
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的流量信息
flowUsageData.value = {
totalFlow: res.data.package_total_mb || 0,
usedFlow: res.data.package_used_mb || 0,
remainFlow: res.data.package_remain_mb || 0,
extend: res.data.extend
}
} else {
ElMessage.error(res.msg || '查询失败')
flowUsageDialogVisible.value = false
}
} catch (error: any) {
console.error('查询流量使用失败:', error)
flowUsageDialogVisible.value = false
} finally {
flowUsageLoading.value = false
}
}
// 查询实名认证状态
const showRealnameStatusDialog = async (iccid: string) => {
realnameStatusDialogVisible.value = true
realnameStatusLoading.value = true
realnameStatusData.value = null
try {
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的实名状态real_name_status: 0未实名 1已实名
let statusText = '未知'
switch (res.data.real_name_status) {
case 0:
statusText = '未实名'
break
case 1:
statusText = '已实名'
break
}
realnameStatusData.value = {
status: statusText,
extend: res.data.extend
}
} else {
ElMessage.error(res.msg || '查询失败')
realnameStatusDialogVisible.value = false
}
} catch (error: any) {
console.error('查询实名状态失败:', error)
realnameStatusDialogVisible.value = false
} finally {
realnameStatusLoading.value = false
}
}
// 查询卡实时状态
const showCardStatusDialog = async (iccid: string) => {
cardStatusDialogVisible.value = true
cardStatusLoading.value = true
cardStatusData.value = null
try {
// 通过 ICCID 解析获取资产信息,resolveAsset 接口已包含所有需要的数据
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
// 直接使用 resolveAsset 返回的网络状态
cardStatusData.value = {
iccid: iccid,
cardStatus: res.data.network_status === 1 ? '正常' : '停机',
extend: res.data.extend
}
} else {
ElMessage.error(res.msg || '查询失败')
cardStatusDialogVisible.value = false
}
} catch (error: any) {
console.error('查询卡状态失败:', error)
cardStatusDialogVisible.value = false
} finally {
cardStatusLoading.value = false
}
}
// 启用此卡(复机)
const handleStartCard = (iccid: string) => {
ElMessageBox.confirm('确定要启用该卡片吗?', '确认启用', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
// 使用统一接口:通过 identifier (ICCID) 启用资产
const res = await AssetService.startAsset(iccid)
if (res.code === 0) {
ElMessage.success('启用成功')
await getTableData()
}
} catch (error: any) {
console.error('启用此卡失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
// 停用此卡(停机)
const handleStopCard = (iccid: string) => {
ElMessageBox.confirm('确定要停用该卡片吗?', '确认停用', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
// 使用统一接口:通过 identifier (ICCID) 停用资产
const res = await AssetService.stopAsset(iccid)
if (res.code === 0) {
ElMessage.success('停用成功')
await getTableData()
}
} catch (error: any) {
console.error('停用此卡失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
// 手动停用 IoT 卡(资产层面的停用)
const handleManualDeactivate = (iccid: string) => {
ElMessageBox.confirm(
`确定要手动停用卡片 ${iccid} 吗?此操作将在资产管理层面停用该卡。`,
'确认手动停用',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
const res = await AssetService.deactivateAsset(iccid)
if (res.code === 0) {
ElMessage.success('手动停用成功')
await getTableData()
}
} catch (error: any) {
console.error('手动停用失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>