Files
one-pipe-system/src/views/asset-management/iot-card-management/index.vue
break 0c3065f970
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
让凭证数组与套餐作废任务进入可联调状态
Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传
  purpose 只能使用 attachment。
  Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。
  Confidence: high
  Scope-risk: broad
  Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与
  order_package_invalidate_task:create/detail 按钮权限。
  Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。
  Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
2026-06-23 09:50:12 +08:00

2946 lines
94 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="90px"
@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
type="primary"
:disabled="selectedCards.length === 0"
@click="showEnterpriseCardAuthorizeDialog"
v-permission="'iot_card:authorize_enterprise'"
>
授权卡给企业
</ElButton>
<ElButton
type="warning"
:disabled="selectedCards.length === 0"
@click="showEnterpriseCardRecallDialog"
v-permission="'iot_card:recall_enterprise_authorization'"
>
回收卡授权
</ElButton>
<ElButton type="primary" @click="showExportDialog" v-permission="'iot_card:export'">
导出
</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">
<ElCascader
v-model="allocateForm.to_shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择目标店铺"
clearable
filterable
style="width: 100%"
/>
</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>
<div
v-if="
allocateForm.selection_type === CardSelectionType.RANGE && allocateRangeCountText
"
class="range-allocation-hint range-allocation-hint--card-form"
>
共分配{{ allocateRangeCountText }}张卡
</div>
<ElFormItem
v-if="allocateForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="allocateForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</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 && userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="allocateForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</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>
<div
v-if="recallForm.selection_type === CardSelectionType.RANGE && recallRangeCountText"
class="range-allocation-hint range-allocation-hint--card-form"
>
共分配{{ recallRangeCountText }}张卡
</div>
<ElFormItem
v-if="recallForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="recallForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="
recallForm.selection_type === CardSelectionType.FILTER && userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="recallForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</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="enterpriseCardAuthorizeDialogVisible"
title="授权卡给企业"
width="640px"
@close="handleEnterpriseCardAuthorizeDialogClose"
>
<ElForm
ref="enterpriseCardAuthorizeFormRef"
:model="enterpriseCardAuthorizeForm"
:rules="enterpriseCardAuthorizeRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseCardAuthorizeForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseCardAuthorizeForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput
v-model="enterpriseCardAuthorizeForm.iccid_start"
placeholder="请输入起始ICCID"
/>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput
v-model="enterpriseCardAuthorizeForm.iccid_end"
placeholder="请输入结束ICCID"
/>
</ElFormItem>
<div
v-if="
enterpriseCardAuthorizeForm.selection_type === CardSelectionType.RANGE &&
enterpriseCardAuthorizeRangeCountText
"
class="range-allocation-hint range-allocation-hint--card-form"
>
共授权{{ enterpriseCardAuthorizeRangeCountText }}张卡
</div>
<template
v-if="enterpriseCardAuthorizeForm.selection_type === CardSelectionType.FILTER"
>
<ElFormItem label="运营商">
<ElSelect
v-model="enterpriseCardAuthorizeForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="店铺">
<ElCascader
v-model="enterpriseCardAuthorizeForm.shop_id_path"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺"
style="width: 100%"
clearable
filterable
/>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseCardAuthorizeForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="ICCID">
<ElInput v-model="enterpriseCardAuthorizeForm.iccid" placeholder="请输入ICCID" />
</ElFormItem>
</template>
<ElFormItem label="备注">
<ElInput
v-model="enterpriseCardAuthorizeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseCardAuthorizeDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseCardAuthorize"
:loading="enterpriseCardAuthorizeLoading"
>
确认授权
</ElButton>
</div>
</template>
</ElDialog>
<!-- 企业卡授权回收对话框 -->
<ElDialog
v-model="enterpriseCardRecallDialogVisible"
title="回收卡授权"
width="640px"
@close="handleEnterpriseCardRecallDialogClose"
>
<ElForm
ref="enterpriseCardRecallFormRef"
:model="enterpriseCardRecallForm"
:rules="enterpriseCardRecallRules"
label-width="120px"
>
<ElFormItem label="授权企业" prop="enterprise_id">
<ElSelect
v-model="enterpriseCardRecallForm.enterprise_id"
placeholder="请选择或搜索企业"
clearable
filterable
remote
reserve-keyword
:remote-method="handleSearchEnterprise"
:loading="enterpriseLoading"
style="width: 100%"
>
<ElOption
v-for="item in enterpriseOptions"
:key="item.id"
:label="item.enterprise_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="enterpriseCardRecallForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput
v-model="enterpriseCardRecallForm.iccid_start"
placeholder="请输入起始ICCID"
/>
</ElFormItem>
<ElFormItem
v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="enterpriseCardRecallForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<div
v-if="
enterpriseCardRecallForm.selection_type === CardSelectionType.RANGE &&
enterpriseCardRecallRangeCountText
"
class="range-allocation-hint range-allocation-hint--card-form"
>
共回收{{ enterpriseCardRecallRangeCountText }}张卡
</div>
<template v-if="enterpriseCardRecallForm.selection_type === CardSelectionType.FILTER">
<ElFormItem label="运营商">
<ElSelect
v-model="enterpriseCardRecallForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="批次号">
<ElSelect
v-model="enterpriseCardRecallForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="ICCID">
<ElInput v-model="enterpriseCardRecallForm.iccid" placeholder="请输入ICCID" />
</ElFormItem>
</template>
<ElFormItem label="备注">
<ElInput
v-model="enterpriseCardRecallForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="enterpriseCardRecallDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleEnterpriseCardRecall"
:loading="enterpriseCardRecallLoading"
>
确认回收
</ElButton>
</div>
</template>
</ElDialog>
<!-- 企业卡授权操作结果 -->
<ElDialog
v-model="enterpriseCardResultDialogVisible"
:title="enterpriseCardResultTitle"
width="700px"
>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="成功数">
<ElTag type="success">{{ enterpriseCardOperationResult.success_count }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="失败数">
<ElTag type="danger">{{ enterpriseCardOperationResult.fail_count }}</ElTag>
</ElDescriptionsItem>
</ElDescriptions>
<div
v-if="
enterpriseCardOperationResult.failed_items &&
enterpriseCardOperationResult.failed_items.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">失败项详情</ElDivider>
<ElTable :data="enterpriseCardOperationResult.failed_items" border max-height="300">
<ElTableColumn prop="iccid" label="ICCID" width="200" />
<ElTableColumn prop="reason" label="失败原因" />
</ElTable>
</div>
<div
v-if="
enterpriseCardOperationResult.recalled_devices &&
enterpriseCardOperationResult.recalled_devices.length > 0
"
style="margin-top: 20px"
>
<ElDivider content-position="left">连带回收设备</ElDivider>
<ElTable :data="enterpriseCardOperationResult.recalled_devices" border max-height="220">
<ElTableColumn prop="virtual_no" label="设备虚拟号" width="180" />
<ElTableColumn prop="card_count" label="关联卡数" width="100" />
<ElTableColumn label="ICCID列表">
<template #default="{ row }">
{{ row.iccids?.join(', ') || '-' }}
</template>
</ElTableColumn>
</ElTable>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" @click="enterpriseCardResultDialogVisible = false"
>确定</ElButton
>
</div>
</template>
</ElDialog>
<!-- 批量设置套餐系列绑定对话框 -->
<ElDialog
v-model="seriesBindingDialogVisible"
title="批量设置套餐系列绑定"
width="600px"
@close="handleSeriesBindingDialogClose"
>
<ElForm
ref="seriesBindingFormRef"
:model="seriesBindingForm"
:rules="seriesBindingRules"
label-width="100"
>
<ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="seriesBindingForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="seriesBindingForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<div
v-if="
seriesBindingForm.selection_type === CardSelectionType.RANGE &&
seriesBindingRangeCountText
"
class="range-allocation-hint range-allocation-hint--card"
>
共分配{{ seriesBindingRangeCountText }}张卡
</div>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="seriesBindingForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="seriesBindingForm.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="
seriesBindingForm.selection_type === CardSelectionType.FILTER &&
userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</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
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="MSISDN">{{
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="创建时间">{{
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>
<!-- 实名认证策略对话框 -->
<RealnamePolicyDialog
v-model="realnamePolicyDialogVisible"
:asset-identifier="currentRealnamePolicyIccid"
:current-policy="currentRealnamePolicy"
@confirm="handleConfirmRealnamePolicy"
/>
<!-- 更新实名状态对话框 -->
<UpdateRealnameStatusDialog
v-model="updateRealnameStatusDialogVisible"
:asset-identifier="currentUpdateRealnameStatusIccid"
:current-realname-status="currentUpdateRealnameStatus"
@success="handleUpdateRealnameStatusSuccess"
/>
<!-- 操作审计日志弹窗 -->
<OperationLogsDialog
v-model="operationLogsDialogVisible"
:identifier="operationLogsIdentifier"
download-permission="iot_card:download_log_file"
/>
<ExportTaskCreateDialog
v-model="exportDialogVisible"
scene="iot_card"
:query="exportQuery"
confirm-permission="iot_card:export"
title="导出IoT卡"
/>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { computed, h } from 'vue'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import {
CardService,
ShopService,
PackageSeriesService,
AssetService,
CarrierService,
EnterpriseService
} from '@/api/modules'
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
import UpdateRealnameStatusDialog from '@/components/business/UpdateRealnameStatusDialog.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { formatDateTime } from '@/utils/business/format'
import type {
StandaloneIotCard,
StandaloneCardStatus,
AllocateStandaloneCardsRequest,
RecallStandaloneCardsRequest,
AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse
} from '@/types/api/card'
import { CardSelectionType } from '@/types/api/card'
import type { PackageSeriesResponse } from '@/types/api'
import type { EnterpriseItem } from '@/types/api/enterprise'
import type { AllocateCardsResponse, RecallCardsResponse } from '@/types/api/enterpriseCard'
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
defineOptions({ name: 'StandaloneCardList' })
const { hasAuth } = useAuth()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
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 enterpriseCardAuthorizeDialogVisible = ref(false)
const enterpriseCardRecallDialogVisible = ref(false)
const enterpriseCardResultDialogVisible = ref(false)
const enterpriseCardAuthorizeLoading = ref(false)
const enterpriseCardRecallLoading = ref(false)
const enterpriseCardResultTitle = ref('')
const enterpriseCardAuthorizeFormRef = ref<FormInstance>()
const enterpriseCardRecallFormRef = ref<FormInstance>()
const enterpriseOptions = ref<EnterpriseItem[]>([])
const enterpriseLoading = ref(false)
const enterpriseCardOperationResult = ref<
Partial<AllocateCardsResponse & RecallCardsResponse> & {
success_count: number
fail_count: number
}
>({
success_count: 0,
fail_count: 0,
failed_items: null,
recalled_devices: null
})
// 操作审计日志弹窗
const operationLogsDialogVisible = ref(false)
const operationLogsIdentifier = ref('')
const exportDialogVisible = ref(false)
// 套餐系列绑定相关
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 createInitialSeriesBindingState = () => ({
selection_type: CardSelectionType.LIST,
series_id: undefined as number | undefined,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
status: undefined as StandaloneCardStatus | undefined,
batch_no: ''
})
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const createInitialEnterpriseCardAuthorizeState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: CardSelectionType.LIST,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
shop_id_path: undefined as number[] | undefined,
batch_no: '',
iccid: '',
remark: ''
})
const createInitialEnterpriseCardRecallState = () => ({
enterprise_id: undefined as number | undefined,
selection_type: CardSelectionType.LIST,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
batch_no: '',
iccid: '',
remark: ''
})
const enterpriseCardAuthorizeForm = reactive(createInitialEnterpriseCardAuthorizeState())
const enterpriseCardRecallForm = reactive(createInitialEnterpriseCardRecallState())
const parseNumericStringToBigInt = (value: string): bigint | null => {
const normalizedValue = value.trim()
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
return null
}
try {
return BigInt(normalizedValue)
} catch {
return null
}
}
const getPositiveRangeCountText = (start: bigint | null, end: bigint | null): string => {
if (start === null || end === null || end < start) {
return ''
}
return (end - start + 1n).toString()
}
const formatDigitGrouping = (value: string): string => value.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
const getFormattedRangeCountText = (startValue?: string, endValue?: string): string => {
const countText = getPositiveRangeCountText(
parseNumericStringToBigInt(startValue ?? ''),
parseNumericStringToBigInt(endValue ?? '')
)
return countText ? formatDigitGrouping(countText) : ''
}
const allocateRangeCountText = computed(() =>
getFormattedRangeCountText(allocateForm.iccid_start, allocateForm.iccid_end)
)
const recallRangeCountText = computed(() =>
getFormattedRangeCountText(recallForm.iccid_start, recallForm.iccid_end)
)
const enterpriseCardAuthorizeRangeCountText = computed(() =>
getFormattedRangeCountText(
enterpriseCardAuthorizeForm.iccid_start,
enterpriseCardAuthorizeForm.iccid_end
)
)
const enterpriseCardRecallRangeCountText = computed(() =>
getFormattedRangeCountText(
enterpriseCardRecallForm.iccid_start,
enterpriseCardRecallForm.iccid_end
)
)
const seriesBindingRangeCountText = computed(() =>
getFormattedRangeCountText(seriesBindingForm.iccid_start, seriesBindingForm.iccid_end)
)
const createEnterpriseCardRules = (form: { selection_type: CardSelectionType }) =>
reactive<FormRules>({
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (form.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (form.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const enterpriseCardAuthorizeRules = createEnterpriseCardRules(enterpriseCardAuthorizeForm)
const enterpriseCardRecallRules = createEnterpriseCardRules(enterpriseCardRecallForm)
const seriesBindingRules = reactive<FormRules>({
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
success_count: 0,
fail_count: 0,
failed_items: null
})
// 运营商搜索相关
const carrierLoading = ref(false)
const searchCarrierOptions = ref<any[]>([])
// 批量分配对话框运营商选项
const allocateCarrierOptions = ref<any[]>([])
const allocateCarrierLoading = ref(false)
// 加载搜索栏运营商选项默认加载10条
const loadSearchCarrierOptions = async (carrierName?: string) => {
try {
const params: any = {
page: 1,
page_size: 10
}
if (carrierName) {
params.carrier_name = carrierName
}
const res = await CarrierService.getCarriers(params)
if (res.code === 0) {
searchCarrierOptions.value = res.data.items
}
} catch (error) {
console.error('加载运营商选项失败:', error)
}
}
// 加载批量分配对话框运营商选项
const loadAllocateCarrierOptions = async (carrierName?: string) => {
try {
allocateCarrierLoading.value = true
const params: any = {
page: 1,
page_size: 20
}
if (carrierName) {
params.carrier_name = carrierName
}
const res = await CarrierService.getCarriers(params)
if (res.code === 0) {
allocateCarrierOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载运营商选项失败:', error)
} finally {
allocateCarrierLoading.value = false
}
}
// 搜索批量分配对话框运营商
const handleSearchAllocateCarrier = (query: string) => {
if (query) {
loadAllocateCarrierOptions(query)
} else {
loadAllocateCarrierOptions()
}
}
// 搜索运营商(用于搜索栏)
const handleSearchCarrier = (query: string) => {
if (query) {
loadSearchCarrierOptions(query)
} else {
loadSearchCarrierOptions()
}
}
// 批量分配对话框批次号选项
const allocateBatchNoOptions = ref<any[]>([])
// 加载批量分配对话框批次号选项
const loadAllocateBatchNoOptions = async (batchNo?: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (batchNo) {
params.batch_no = batchNo
}
const res = await CardService.getIotCardImportTasks(params)
if (res.code === 0) {
allocateBatchNoOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载批次号选项失败:', error)
}
}
// 搜索批量分配对话框批次号
const handleSearchAllocateBatchNo = (query: string) => {
if (query) {
loadAllocateBatchNoOptions(query)
} else {
loadAllocateBatchNoOptions()
}
}
// 搜索栏远程选项
const shopOptions = ref<any[]>([])
const searchSeriesOptions = ref<any[]>([])
// 搜索店铺(用于搜索栏)
const handleSearchShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
const handleSearchEnterprise = async (query?: string) => {
try {
enterpriseLoading.value = true
const params = {
page: 1,
page_size: 20,
enterprise_name: query || undefined,
status: 1
}
const res = await EnterpriseService.getEnterprises(params)
if (res.code === 0) {
enterpriseOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索企业失败:', error)
} finally {
enterpriseLoading.value = false
}
}
// 搜索套餐系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
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)
}
}
// 卡详情弹窗相关
const cardDetailDialogVisible = ref(false)
const cardDetailLoading = ref(false)
const currentCardDetail = ref<any>(null)
// IoT卡操作相关对话框
// 实名认证策略对话框
const realnamePolicyDialogVisible = ref(false)
const currentRealnamePolicyIccid = ref('')
const currentRealnamePolicy = ref('none')
// 更新实名状态对话框
const updateRealnameStatusDialogVisible = ref(false)
const currentUpdateRealnameStatusIccid = ref('')
const currentUpdateRealnameStatus = ref<number>(0)
// 显示实名认证策略对话框
const handleShowRealnamePolicy = async (iccid: string, currentPolicy?: string) => {
currentRealnamePolicyIccid.value = iccid
if (currentPolicy !== undefined && currentPolicy !== null) {
currentRealnamePolicy.value = String(currentPolicy)
} else {
try {
const res = await AssetService.resolveAsset(iccid)
if (res.code === 0 && res.data) {
currentRealnamePolicy.value = res.data.realname_policy ?? 'none'
} else {
currentRealnamePolicy.value = 'none'
}
} catch {
currentRealnamePolicy.value = 'none'
}
}
realnamePolicyDialogVisible.value = true
}
// 确认设置实名认证策略
const handleConfirmRealnamePolicy = async (data: { realname_policy: string }) => {
if (!currentRealnamePolicyIccid.value) {
ElMessage.warning('未指定卡片')
return
}
try {
const res = await AssetService.updateRealnamePolicy(
currentRealnamePolicyIccid.value,
data.realname_policy
)
if (res.code === 0) {
ElMessage.success('设置实名认证策略成功')
realnamePolicyDialogVisible.value = false
await getTableData()
} else {
ElMessage.error(res.msg || '设置失败')
}
} catch (error) {
console.error('设置实名认证策略失败:', error)
}
}
// 显示更新实名状态对话框
const handleShowUpdateRealnameStatus = (iccid: string, realnameStatus: number) => {
currentUpdateRealnameStatusIccid.value = iccid
currentUpdateRealnameStatus.value = realnameStatus
updateRealnameStatusDialogVisible.value = true
}
// 更新实名状态成功
const handleUpdateRealnameStatusSuccess = async () => {
await getTableData()
}
// 店铺相关
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
lazy: true,
checkStrictly: true,
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
const loadTopLevelShopCascadeOptions = async () => {
try {
const res = await ShopService.getShopsCascade({ parent_id: undefined })
if (res.code === 0) {
shopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载顶级店铺失败:', error)
}
}
// 搜索表单初始值
const initialSearchState: {
status: undefined | number
network_status: undefined | number
iccid: string
carrier_name: string
msisdn: string
virtual_no: string
device_virtual_no: string
shop_id: undefined | number
series_id: undefined | number
has_active_package: undefined | boolean
is_distributed: undefined | number
is_standalone: undefined | boolean
authorized_enterprise_id: undefined | number
is_authorized_to_enterprise: undefined | boolean
[key: string]: any
} = {
status: undefined,
network_status: undefined,
iccid: '',
carrier_name: '',
msisdn: '',
virtual_no: '',
device_virtual_no: '',
shop_id: undefined,
series_id: undefined,
has_active_package: undefined,
is_distributed: undefined,
is_standalone: undefined,
authorized_enterprise_id: undefined,
is_authorized_to_enterprise: undefined,
is_replaced: undefined,
iccid_start: '',
iccid_end: ''
}
// 搜索表单
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 baseFormItems: 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: 'network_status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '停机', value: 0 },
{ label: '正常', value: 1 }
]
},
{
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: 'shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchShops,
loading: false,
placeholder: '请选择或搜索店铺名称'
},
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
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: 'iccid_start',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID起始号'
}
},
{
label: '结束号',
prop: 'iccid_end',
type: 'input',
config: {
clearable: true,
placeholder: '请输入ICCID结束号'
}
},
{
label: '是否绑设备',
prop: 'is_standalone',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '未绑定设备', value: true },
{ label: '已绑定设备', value: false }
]
}
]
// 搜索表单配置
baseFormItems.splice(8, 0, {
label: '有效套餐',
prop: 'has_active_package',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '是', value: true },
{ label: '否', value: false }
]
})
baseFormItems.splice(9, 0, {
label: '授权企业',
prop: 'authorized_enterprise_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchEnterprise,
loading: enterpriseLoading.value,
placeholder: '请选择或搜索企业'
},
options: () =>
enterpriseOptions.value.map((enterprise) => ({
label: enterprise.enterprise_name,
value: enterprise.id
}))
})
baseFormItems.splice(10, 0, {
label: '授权状态',
prop: 'is_authorized_to_enterprise',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已授权', value: true },
{ label: '未授权', value: false }
]
})
const formItems: SearchFormItem[] = baseFormItems
// 列配置
const columnOptions = [
{ label: 'ICCID', prop: 'iccid' },
{ label: 'IMEI', prop: 'gateway_card_imei' },
{ label: 'MSISDN', prop: 'msisdn' },
{ label: '卡虚拟号', prop: 'virtual_no' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '状态', prop: 'status' },
{ label: '资产世代', prop: 'generation' },
{ label: '业务状态', prop: 'asset_status' },
{ label: '激活状态', prop: 'activation_status' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '网络状态', prop: 'network_status' },
{ label: '实名状态', prop: 'real_name_status' },
{ label: '实名认证策略', prop: 'realname_policy' },
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '自然月累计流量(MB)', prop: 'current_month_usage_mb' },
{ label: '本月开始日期', prop: 'current_month_start_date' },
{ label: '上月流量总量(MB)', prop: 'last_month_total_mb' },
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
{ label: '最后流量检查时间', prop: 'last_data_check_at' },
{ label: '最后实名检查时间', prop: 'last_real_name_check_at' },
{ label: '启用轮询', prop: 'enable_polling' },
{ label: '批次号', prop: 'batch_no' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_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: RoutesAlias.AssetInformation,
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 { 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: 'gateway_card_imei',
label: 'IMEI',
width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.gateway_card_imei || '-'
},
{
prop: 'msisdn',
label: 'MSISDN',
width: 160
},
{
prop: 'virtual_no',
label: '卡虚拟号',
width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.virtual_no || '-'
},
{
prop: 'device_virtual_no',
label: '绑定设备虚拟号',
width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.device_virtual_no || '-'
},
{
prop: 'card_category',
label: '卡业务类型',
width: 100,
formatter: (row: StandaloneIotCard) => getCardCategoryText(row.card_category)
},
{
prop: 'carrier_name',
label: '运营商',
width: 150,
showOverflowTooltip: true
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.shop_name || '-'
},
{
prop: 'series_name',
label: '套餐系列',
width: 180,
showOverflowTooltip: true,
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: 'generation',
label: '资产世代',
width: 100,
formatter: (row: StandaloneIotCard) => row.generation ?? '-'
},
{
prop: 'asset_status',
label: '业务状态',
width: 100,
formatter: (row: StandaloneIotCard) => {
const assetStatusMap: Record<number, { text: string; type: any }> = {
1: { text: '在库', type: 'info' },
2: { text: '已销售', type: 'success' },
3: { text: '已换货', type: 'warning' },
4: { text: '已停用', type: 'danger' }
}
if (row.asset_status == null) return '-'
const status = assetStatusMap[row.asset_status] || { text: '未知', type: 'info' }
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
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: 'realname_policy',
label: '实名认证策略',
width: 150,
formatter: (row: StandaloneIotCard) => {
const policy = row.realname_policy || ''
const policyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
const text = policyMap[policy] || (policy ? policy : '未知')
return text
}
},
{
prop: 'activated_at',
label: '激活时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.activated_at ? formatDateTime(row.activated_at) : '-'
},
{
prop: 'current_month_usage_mb',
label: '自然月累计流量(MB)',
width: 180,
formatter: (row: StandaloneIotCard) => row.current_month_usage_mb?.toLocaleString() || '0'
},
{
prop: 'current_month_start_date',
label: '本月开始日期',
width: 200,
formatter: (row: StandaloneIotCard) =>
row.current_month_start_date ? formatDateTime(row.current_month_start_date) : '-'
},
{
prop: 'last_month_total_mb',
label: '上月流量总量(MB)',
width: 150,
formatter: (row: StandaloneIotCard) => row.last_month_total_mb?.toLocaleString() || '0'
},
{
prop: 'data_usage_mb',
label: '累计流量(MB)',
width: 120,
formatter: (row: StandaloneIotCard) => row.data_usage_mb?.toLocaleString() || '0'
},
{
prop: 'last_data_check_at',
label: '最后流量检查时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.last_data_check_at ? formatDateTime(row.last_data_check_at) : '-'
},
{
prop: 'last_real_name_check_at',
label: '最后实名检查时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.last_real_name_check_at ? formatDateTime(row.last_real_name_check_at) : '-'
},
{
prop: 'enable_polling',
label: '启用轮询',
width: 100,
formatter: (row: StandaloneIotCard) => (row.enable_polling ? '是' : '否')
},
{
prop: 'batch_no',
label: '批次号',
width: 200,
showOverflowTooltip: true
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
}
])
// 操作列配置
const getActions = (row: StandaloneIotCard) => {
const actions: any[] = []
if (row.network_status === 1) {
if (hasAuth('iot_card:stop_card')) {
actions.push({
label: '停用此卡',
handler: () => handleCardOperation('stop-card', row.iccid),
type: 'primary'
})
}
} else {
if (hasAuth('iot_card:start_card')) {
actions.push({
label: '启用此卡',
handler: () => handleCardOperation('start-card', row.iccid),
type: 'primary'
})
}
}
if (hasAuth('iot_card:realname_policy')) {
actions.push({
label: '实名认证策略',
handler: () => handleCardOperation('realname-policy', row.iccid, row),
type: 'primary'
})
}
if (hasAuth('iot_card:update_realname_status')) {
actions.push({
label: '更新实名状态',
handler: () => handleCardOperation('update-realname-status', row.iccid, row),
type: 'primary'
})
}
if (hasAuth('iot_card:clear_series')) {
actions.push({
label: '清除关联',
handler: () => handleCardOperation('clear-series', row.iccid),
type: 'primary'
})
}
if (hasAuth('iot_card:operation_logs')) {
actions.push({
label: '操作审计日志',
handler: () => {
operationLogsIdentifier.value = row.iccid
operationLogsDialogVisible.value = true
},
type: 'primary'
})
}
return actions
}
const exportQuery = computed(() => {
const query: Record<string, unknown> = {}
Object.entries(formFilters).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return
if (Array.isArray(value) && value.length === 0) return
query[key] = value
})
return query
})
const showExportDialog = () => {
exportDialogVisible.value = true
}
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 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: ''
})
loadAllocateCarrierOptions()
loadAllocateBatchNoOptions()
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()
}
loadAllocateCarrierOptions()
loadAllocateBatchNoOptions()
}
// 关闭批量分配对话框
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: Array.isArray(allocateForm.to_shop_id)
? allocateForm.to_shop_id[allocateForm.to_shop_id.length - 1]
: 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 getInferredCardEnterpriseId = () => {
const filterEnterpriseId = formFilters.authorized_enterprise_id
if (filterEnterpriseId) return filterEnterpriseId
const selectedEnterpriseIds = Array.from(
new Set(
selectedCards.value
.map((card) => card.authorized_enterprise_id)
.filter((id): id is number => typeof id === 'number')
)
)
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
}
const resetEnterpriseCardAuthorizeForm = () => {
Object.assign(enterpriseCardAuthorizeForm, createInitialEnterpriseCardAuthorizeState(), {
selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER,
carrier_id: undefined,
shop_id_path: undefined,
batch_no: formFilters.batch_no || '',
iccid: formFilters.iccid || ''
})
}
const resetEnterpriseCardRecallForm = () => {
Object.assign(enterpriseCardRecallForm, createInitialEnterpriseCardRecallState(), {
enterprise_id: getInferredCardEnterpriseId(),
selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER,
carrier_id: undefined,
batch_no: formFilters.batch_no || '',
iccid: formFilters.iccid || ''
})
}
const showEnterpriseCardAuthorizeDialog = async () => {
resetEnterpriseCardAuthorizeForm()
await Promise.all([
handleSearchEnterprise(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions(),
loadTopLevelShopCascadeOptions()
])
enterpriseCardAuthorizeDialogVisible.value = true
enterpriseCardAuthorizeFormRef.value?.clearValidate()
}
const showEnterpriseCardRecallDialog = async () => {
resetEnterpriseCardRecallForm()
await Promise.all([
handleSearchEnterprise(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
enterpriseCardRecallDialogVisible.value = true
enterpriseCardRecallFormRef.value?.clearValidate()
}
const handleEnterpriseCardAuthorizeDialogClose = () => {
Object.assign(enterpriseCardAuthorizeForm, createInitialEnterpriseCardAuthorizeState())
enterpriseCardAuthorizeFormRef.value?.resetFields()
}
const handleEnterpriseCardRecallDialogClose = () => {
Object.assign(enterpriseCardRecallForm, createInitialEnterpriseCardRecallState())
enterpriseCardRecallFormRef.value?.resetFields()
}
const appendEnterpriseCardFilterParams = (params: Record<string, unknown>, form: any) => {
if (form.carrier_id) params.carrier_id = form.carrier_id
if (Array.isArray(form.shop_id_path) && form.shop_id_path.length > 0) {
params.shop_id = form.shop_id_path[form.shop_id_path.length - 1]
}
if (form.batch_no) params.batch_no = form.batch_no
if (form.iccid) params.iccid = form.iccid
}
const buildEnterpriseCardParams = (form: any) => {
const params: Record<string, unknown> = {
selection_type: form.selection_type,
remark: form.remark || undefined
}
if (form.selection_type === CardSelectionType.LIST) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (!iccids.length) {
ElMessage.warning('列表模式下请先选择卡')
return null
}
params.iccids = iccids
} else if (form.selection_type === CardSelectionType.RANGE) {
params.iccid_start = form.iccid_start
params.iccid_end = form.iccid_end
} else {
appendEnterpriseCardFilterParams(params, form)
}
Object.keys(params).forEach((key) => {
if (params[key] === undefined || params[key] === '') delete params[key]
})
return params
}
const applyEnterpriseCardOperationSuccess = async (
title: string,
result: AllocateCardsResponse | RecallCardsResponse
) => {
enterpriseCardOperationResult.value = result
enterpriseCardResultTitle.value = title
enterpriseCardResultDialogVisible.value = true
selectedCards.value = []
await getTableData()
}
const handleEnterpriseCardAuthorize = async () => {
if (!enterpriseCardAuthorizeFormRef.value) return
await enterpriseCardAuthorizeFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseCardParams(enterpriseCardAuthorizeForm)
if (!params) return
enterpriseCardAuthorizeLoading.value = true
try {
const res = await EnterpriseService.allocateCards(
enterpriseCardAuthorizeForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseCardAuthorizeDialogVisible.value = false
await applyEnterpriseCardOperationSuccess('授权卡给企业结果', res.data)
}
} catch (error) {
console.error('授权卡给企业失败:', error)
} finally {
enterpriseCardAuthorizeLoading.value = false
}
})
}
const handleEnterpriseCardRecall = async () => {
if (!enterpriseCardRecallFormRef.value) return
await enterpriseCardRecallFormRef.value.validate(async (valid) => {
if (!valid) return
const params = buildEnterpriseCardParams(enterpriseCardRecallForm)
if (!params) return
enterpriseCardRecallLoading.value = true
try {
const res = await EnterpriseService.recallCards(
enterpriseCardRecallForm.enterprise_id!,
params as any
)
if (res.code === 0) {
enterpriseCardRecallDialogVisible.value = false
await applyEnterpriseCardOperationSuccess('回收卡授权结果', res.data)
}
} catch (error) {
console.error('回收卡授权失败:', error)
} finally {
enterpriseCardRecallLoading.value = false
}
})
}
// 显示套餐系列绑定对话框
const showSeriesBindingDialog = async () => {
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
selection_type:
selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER
})
await Promise.all([
loadPackageSeriesList(),
loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
seriesBindingDialogVisible.value = true
seriesBindingFormRef.value?.clearValidate()
}
// 加载套餐系列列表支持名称搜索默认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 = () => {
Object.assign(seriesBindingForm, createInitialSeriesBindingState())
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
}
// 关闭套餐系列绑定结果对话框
const handleSeriesBindingResultDialogClose = () => {
// 刷新列表
getTableData()
}
// 执行套餐系列绑定
const handleSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return
await seriesBindingFormRef.value.validate(async (valid) => {
if (valid) {
seriesBindingLoading.value = true
try {
const request: BatchSetCardSeriesBindingRequest = {
selection_type: seriesBindingForm.selection_type,
series_id: seriesBindingForm.series_id!
}
if (seriesBindingForm.selection_type === CardSelectionType.LIST) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('列表模式下请先选择要设置的卡')
return
}
request.iccids = iccids
} else if (seriesBindingForm.selection_type === CardSelectionType.RANGE) {
request.iccid_start = seriesBindingForm.iccid_start
request.iccid_end = seriesBindingForm.iccid_end
} else {
if (seriesBindingForm.carrier_id) request.carrier_id = seriesBindingForm.carrier_id
if (seriesBindingForm.status) request.status = seriesBindingForm.status
if (seriesBindingForm.batch_no) request.batch_no = seriesBindingForm.batch_no
}
const res = await CardService.batchSetCardSeriesBinding(request)
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
}
}
})
}
// IoT卡操作处理函数
const handleCardOperation = (command: string, iccid: string, row?: any) => {
switch (command) {
case 'start-card':
handleStartCard(iccid)
break
case 'stop-card':
handleStopCard(iccid)
break
case 'realname-policy':
handleShowRealnamePolicy(iccid, row?.realname_policy)
break
case 'update-realname-status':
handleShowUpdateRealnameStatus(iccid, row?.real_name_status)
break
case 'clear-series':
handleClearSingleCardSeries(iccid)
break
}
}
// 清除单卡套餐系列绑定
const handleClearSingleCardSeries = (iccid: string) => {
ElMessageBox.confirm(`确定要清除该卡(${iccid})的套餐系列关联吗?`, '确认清除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
const res = await CardService.batchSetCardSeriesBinding({
selection_type: CardSelectionType.LIST,
iccids: [iccid],
series_id: 0
})
if (res.code === 0) {
if (res.data.fail_count === 0) {
ElMessage.success('清除关联成功')
} else {
ElMessage.warning(`失败:${res.data.failed_items?.[0]?.reason || '未知原因'}`)
}
await getTableData()
}
} catch (error) {
console.error('清除关联失败:', error)
}
})
.catch(() => {
// 用户取消
})
}
// 启用此卡(复机)
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(() => {
// 用户取消
})
}
</script>
<style lang="scss" scoped>
.range-allocation-hint {
font-size: 13px;
line-height: 20px;
color: var(--el-text-color-secondary);
}
.range-allocation-hint--card {
margin: -6px 0 18px 100px;
}
.range-allocation-hint--card-form {
margin: -6px 0 18px 120px;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>