3226 lines
104 KiB
Vue
3226 lines
104 KiB
Vue
<template>
|
||
<ArtTableFullScreen>
|
||
<div class="device-list-page" id="table-full-screen">
|
||
<!-- 搜索栏 -->
|
||
<ArtSearchBar
|
||
v-model:filter="searchForm"
|
||
:items="searchFormItems"
|
||
@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="primary"
|
||
@click="handleBatchAllocate"
|
||
:disabled="!selectedDevices.length"
|
||
v-permission="'device:batch_allocate'"
|
||
>
|
||
批量分配
|
||
</ElButton>
|
||
<ElButton
|
||
@click="handleBatchRecall"
|
||
:disabled="!selectedDevices.length"
|
||
v-permission="'device:batch_recall'"
|
||
>
|
||
批量回收
|
||
</ElButton>
|
||
<ElButton
|
||
type="info"
|
||
:disabled="!selectedDevices.length"
|
||
@click="handleBatchSetSeries"
|
||
v-permission="'device:batch_set_series'"
|
||
>
|
||
批量设置套餐系列
|
||
</ElButton>
|
||
<ElButton
|
||
type="primary"
|
||
:disabled="!selectedDevices.length"
|
||
@click="showBatchRealnamePolicyDialog"
|
||
v-permission="'device:realname_policy'"
|
||
>
|
||
批量修改实名顺序
|
||
</ElButton>
|
||
<ElButton
|
||
type="primary"
|
||
:disabled="!selectedDevices.length"
|
||
@click="showEnterpriseDeviceAuthorizeDialog"
|
||
v-permission="'devices:authorize_enterprise'"
|
||
>
|
||
授权设备给企业
|
||
</ElButton>
|
||
<ElButton
|
||
type="warning"
|
||
:disabled="!selectedDevices.length"
|
||
@click="showEnterpriseDeviceRecallDialog"
|
||
v-permission="'devices:recall_enterprise_authorization'"
|
||
>
|
||
撤销设备授权
|
||
</ElButton>
|
||
<ElButton type="primary" @click="showExportDialog" v-permission="'devices:export'">
|
||
导出
|
||
</ElButton>
|
||
</template>
|
||
</ArtTableHeader>
|
||
|
||
<!-- 表格 -->
|
||
<ArtTable
|
||
ref="tableRef"
|
||
row-key="id"
|
||
:loading="loading"
|
||
:data="deviceList"
|
||
:currentPage="pagination.page"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:marginTop="10"
|
||
:actions="getActions"
|
||
:actionsWidth="180"
|
||
@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">
|
||
<ElForm
|
||
ref="allocateFormRef"
|
||
:model="allocateForm"
|
||
:rules="allocateRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="已选设备数">
|
||
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> 台
|
||
</ElFormItem>
|
||
<ElFormItem label="店铺" prop="target_shop_id">
|
||
<ElCascader
|
||
v-model="allocateForm.target_shop_id"
|
||
:options="shopCascadeOptions"
|
||
:props="shopCascadeProps"
|
||
placeholder="请选择目标店铺"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="备注">
|
||
<ElInput
|
||
v-model="allocateForm.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入备注信息(选填)"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
|
||
<!-- 分配结果 -->
|
||
<div v-if="allocateResult" style="margin-top: 20px">
|
||
<ElAlert
|
||
:type="allocateResult.fail_count === 0 ? 'success' : 'warning'"
|
||
:closable="false"
|
||
style="margin-bottom: 10px"
|
||
>
|
||
<template #title>
|
||
成功分配 {{ allocateResult.success_count }} 台,失败
|
||
{{ allocateResult.fail_count }} 台
|
||
</template>
|
||
</ElAlert>
|
||
<div v-if="allocateResult.failed_items && allocateResult.failed_items.length > 0">
|
||
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
||
<div
|
||
v-for="item in allocateResult.failed_items"
|
||
:key="item.device_id"
|
||
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||
>
|
||
设备号: {{ item.virtual_no }} - {{ item.reason }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="handleCloseAllocateDialog">
|
||
{{ allocateResult ? '关闭' : '取消' }}
|
||
</ElButton>
|
||
<ElButton
|
||
v-if="!allocateResult"
|
||
type="primary"
|
||
@click="handleConfirmAllocate"
|
||
:loading="allocateLoading"
|
||
>
|
||
确认分配
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 批量回收对话框 -->
|
||
<ElDialog v-model="recallDialogVisible" title="批量回收设备" width="600px">
|
||
<ElForm ref="recallFormRef" :model="recallForm" label-width="120px">
|
||
<ElFormItem label="已选设备数">
|
||
<span style="font-weight: bold; color: #e6a23c">{{ selectedDevices.length }}</span> 台
|
||
</ElFormItem>
|
||
<ElFormItem label="备注">
|
||
<ElInput
|
||
v-model="recallForm.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入备注信息(选填)"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
|
||
<!-- 回收结果 -->
|
||
<div v-if="recallResult" style="margin-top: 20px">
|
||
<ElAlert
|
||
:type="recallResult.fail_count === 0 ? 'success' : 'warning'"
|
||
:closable="false"
|
||
style="margin-bottom: 10px"
|
||
>
|
||
<template #title>
|
||
成功回收 {{ recallResult.success_count }} 台,失败 {{ recallResult.fail_count }} 台
|
||
</template>
|
||
</ElAlert>
|
||
<div v-if="recallResult.failed_items && recallResult.failed_items.length > 0">
|
||
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
||
<div
|
||
v-for="item in recallResult.failed_items"
|
||
:key="item.device_id"
|
||
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||
>
|
||
设备号: {{ item.virtual_no }} - {{ item.reason }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="handleCloseRecallDialog">
|
||
{{ recallResult ? '关闭' : '取消' }}
|
||
</ElButton>
|
||
<ElButton
|
||
v-if="!recallResult"
|
||
type="primary"
|
||
@click="handleConfirmRecall"
|
||
:loading="recallLoading"
|
||
>
|
||
确认回收
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 批量设置套餐系列绑定对话框 -->
|
||
<ElDialog
|
||
v-model="seriesBindingDialogVisible"
|
||
title="批量设置设备套餐系列绑定"
|
||
width="35%"
|
||
@close="handleCloseSeriesBindingDialog"
|
||
>
|
||
<ElForm
|
||
ref="seriesBindingFormRef"
|
||
:model="seriesBindingForm"
|
||
:rules="seriesBindingRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="已选设备数">
|
||
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> 台
|
||
</ElFormItem>
|
||
<ElFormItem label="选设备方式" prop="selection_type">
|
||
<ElRadioGroup v-model="seriesBindingForm.selection_type">
|
||
<ElRadio label="list">设备ID列表</ElRadio>
|
||
<ElRadio label="range">设备号范围</ElRadio>
|
||
<ElRadio label="filter">筛选条件</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.LIST"
|
||
label="设备列表"
|
||
>
|
||
<div>已选择 {{ selectedDevices.length }} 台设备</div>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
|
||
label="起始设备号"
|
||
prop="virtual_no_start"
|
||
>
|
||
<ElInput
|
||
v-model="seriesBindingForm.virtual_no_start"
|
||
placeholder="请输入起始设备号"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
|
||
label="结束设备号"
|
||
prop="virtual_no_end"
|
||
>
|
||
<ElInput v-model="seriesBindingForm.virtual_no_end" placeholder="请输入结束设备号" />
|
||
</ElFormItem>
|
||
|
||
<div
|
||
v-if="
|
||
seriesBindingForm.selection_type === DeviceSelectionType.RANGE &&
|
||
seriesBindingRangeCountText
|
||
"
|
||
class="range-allocation-hint range-allocation-hint--device"
|
||
>
|
||
共分配{{ seriesBindingRangeCountText }}台设备
|
||
</div>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="设备号"
|
||
>
|
||
<ElInput
|
||
v-model="seriesBindingForm.virtual_no"
|
||
placeholder="请输入设备号"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="设备名称"
|
||
>
|
||
<ElInput
|
||
v-model="seriesBindingForm.device_name"
|
||
placeholder="请输入设备名称"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="设备类型"
|
||
>
|
||
<ElInput
|
||
v-model="seriesBindingForm.device_type"
|
||
placeholder="请输入设备类型"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="制造商"
|
||
>
|
||
<ElInput
|
||
v-model="seriesBindingForm.manufacturer"
|
||
placeholder="请输入制造商"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.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 === DeviceSelectionType.FILTER"
|
||
label="批次号"
|
||
>
|
||
<ElSelect
|
||
v-model="seriesBindingForm.batch_no"
|
||
placeholder="请选择或搜索批次号"
|
||
clearable
|
||
filterable
|
||
remote
|
||
:remote-method="handleSearchBatchNo"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="item in searchBatchNoOptions"
|
||
:key="item.id"
|
||
:label="item.batch_no"
|
||
:value="item.batch_no"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="店铺名称"
|
||
>
|
||
<ElCascader
|
||
v-model="seriesBindingForm.shop_id_path"
|
||
:options="seriesBindingShopCascadeOptions"
|
||
:props="seriesBindingShopCascadeProps"
|
||
placeholder="请选择店铺名称"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="当前套餐系列"
|
||
>
|
||
<ElSelect
|
||
v-model="seriesBindingForm.filter_series_id"
|
||
placeholder="请选择或搜索当前套餐系列"
|
||
clearable
|
||
filterable
|
||
remote
|
||
:remote-method="searchSeries"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="series in searchSeriesOptions"
|
||
:key="series.id"
|
||
:label="series.series_name"
|
||
:value="series.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||
label="创建设备时间"
|
||
>
|
||
<ElDatePicker
|
||
v-model="seriesBindingForm.created_at_range"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
/>
|
||
</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>
|
||
|
||
<!-- 设置结果 -->
|
||
<div v-if="seriesBindingResult" style="margin-top: 20px">
|
||
<ElAlert
|
||
:type="seriesBindingResult.fail_count === 0 ? 'success' : 'warning'"
|
||
:closable="false"
|
||
style="margin-bottom: 10px"
|
||
>
|
||
<template #title>
|
||
成功设置 {{ seriesBindingResult.success_count }} 台,失败
|
||
{{ seriesBindingResult.fail_count }} 台
|
||
</template>
|
||
</ElAlert>
|
||
<div
|
||
v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0"
|
||
>
|
||
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
||
<div
|
||
v-for="item in seriesBindingResult.failed_items"
|
||
:key="item.device_id"
|
||
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||
>
|
||
设备号: {{ item.virtual_no }} - {{ item.reason }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="handleCloseSeriesBindingDialog">
|
||
{{ seriesBindingResult ? '关闭' : '取消' }}
|
||
</ElButton>
|
||
<ElButton
|
||
v-if="!seriesBindingResult"
|
||
type="primary"
|
||
@click="handleConfirmSeriesBinding"
|
||
:loading="seriesBindingLoading"
|
||
>
|
||
确认设置
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 设备详情弹窗 -->
|
||
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="1000px">
|
||
<div v-if="deviceDetailLoading" style="padding: 40px 0; text-align: center">
|
||
<ElIcon class="is-loading" :size="40">
|
||
<Loading />
|
||
</ElIcon>
|
||
</div>
|
||
<div v-else-if="currentDeviceDetail">
|
||
<!-- 设备基本信息 -->
|
||
<ElDescriptions :column="3" border style="margin-bottom: 20px">
|
||
<ElDescriptionsItem label="设备ID">{{ currentDeviceDetail.id }}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="设备号" :span="2">{{
|
||
currentDeviceDetail.virtual_no
|
||
}}</ElDescriptionsItem>
|
||
|
||
<ElDescriptionsItem label="设备名称">{{
|
||
currentDeviceDetail.device_name || '-'
|
||
}}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="设备型号">{{
|
||
currentDeviceDetail.device_model || '-'
|
||
}}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="设备类型">{{
|
||
currentDeviceDetail.device_type || '-'
|
||
}}</ElDescriptionsItem>
|
||
|
||
<ElDescriptionsItem label="制造商">{{
|
||
currentDeviceDetail.manufacturer || '-'
|
||
}}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="最大插槽数">{{
|
||
currentDeviceDetail.max_sim_slots
|
||
}}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="已绑定卡数量">{{
|
||
currentDeviceDetail.bound_card_count
|
||
}}</ElDescriptionsItem>
|
||
|
||
<ElDescriptionsItem label="状态">
|
||
<ElTag :type="getDeviceStatusTagType(currentDeviceDetail.status)">
|
||
{{ currentDeviceDetail.status_name }}
|
||
</ElTag>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="批次号" :span="2">{{
|
||
currentDeviceDetail.batch_no || '-'
|
||
}}</ElDescriptionsItem>
|
||
|
||
<ElDescriptionsItem label="创建时间" :span="3">{{
|
||
formatDateTime(currentDeviceDetail.created_at) || '-'
|
||
}}</ElDescriptionsItem>
|
||
</ElDescriptions>
|
||
</div>
|
||
</ElDialog>
|
||
|
||
<!-- 绑定卡片列表弹窗 -->
|
||
<ElDialog v-model="deviceCardsDialogVisible" title="设备绑定的卡" width="60%">
|
||
<!-- 绑定新卡表单 -->
|
||
<ElCard shadow="never" class="bind-card-section" style="margin-bottom: 20px">
|
||
<template #header>
|
||
<div style="font-weight: bold">绑定新卡</div>
|
||
</template>
|
||
<ElForm
|
||
ref="bindCardFormRef"
|
||
:model="bindCardForm"
|
||
:rules="bindCardRules"
|
||
label-width="80"
|
||
>
|
||
<ElRow :gutter="20">
|
||
<ElCol :span="6">
|
||
<ElFormItem label="搜索类型" prop="search_type">
|
||
<ElSelect
|
||
v-model="bindCardForm.search_type"
|
||
placeholder="请选择搜索类型"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption label="ICCID" value="iccid" />
|
||
<ElOption label="运营商名称" value="carrier_name" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
<ElCol :span="9">
|
||
<ElFormItem label="ICCID" prop="iot_card_id">
|
||
<ElSelect
|
||
v-model="bindCardForm.iot_card_id"
|
||
:placeholder="
|
||
bindCardForm.search_type === 'carrier_name'
|
||
? '请选择或搜索运营商名称'
|
||
: '请选择或搜索ICCID'
|
||
"
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="searchIotCards"
|
||
:loading="iotCardSearchLoading"
|
||
clearable
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="card in iotCardList"
|
||
:key="card.id"
|
||
:label="`${card.iccid} ${card.carrier_name ? '(' + card.carrier_name + ')' : ''}`"
|
||
:value="card.id"
|
||
/>
|
||
<ElOption
|
||
v-if="iotCardList.length === 0 && !iotCardSearchLoading"
|
||
label="未找到相关数据"
|
||
value=""
|
||
disabled
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
<ElCol :span="6">
|
||
<ElFormItem label="插槽位置" prop="slot_position">
|
||
<ElSelect
|
||
v-model="bindCardForm.slot_position"
|
||
placeholder="请选择插槽位置"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="i in currentDeviceDetail?.max_sim_slots || 4"
|
||
:key="i"
|
||
:label="`插槽 ${i}`"
|
||
:value="i"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElCol>
|
||
<ElCol :span="3">
|
||
<ElButton
|
||
v-permission="'device:bind_card'"
|
||
type="primary"
|
||
@click="handleConfirmBindCard"
|
||
:loading="bindCardLoading"
|
||
style="width: 100%"
|
||
>
|
||
确认绑定
|
||
</ElButton>
|
||
</ElCol>
|
||
</ElRow>
|
||
</ElForm>
|
||
</ElCard>
|
||
|
||
<!-- 已绑定卡片列表 -->
|
||
<div v-if="deviceCardsLoading" style="padding: 40px 0; text-align: center">
|
||
<ElIcon class="is-loading" :size="40">
|
||
<Loading />
|
||
</ElIcon>
|
||
</div>
|
||
<div v-else>
|
||
<ElTable :data="deviceCards" border style="width: 100%">
|
||
<ElTableColumn prop="slot_position" label="插槽位置" width="120">
|
||
<template #default="{ row }">
|
||
<div style="display: flex; gap: 10px; align-items: center">
|
||
<span>SIM-{{ row.slot_position }}</span>
|
||
<ElTag v-if="row.is_current" type="success" size="small">当前</ElTag>
|
||
</div>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="iccid" label="ICCID" min-width="120" />
|
||
<ElTableColumn prop="msisdn" label="接入号" width="160" />
|
||
<ElTableColumn prop="network_status" label="网络状态" width="100">
|
||
<template #default="{ row }">
|
||
<ElTag :type="getNetworkStatusTagType(row.network_status)">
|
||
{{ getNetworkStatusText(row.network_status) }}
|
||
</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
|
||
<template #default="{ row }">
|
||
{{ formatDateTime(row.bind_time) || '-' }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="操作" width="100" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<ElButton
|
||
v-permission="'device:unbind_card'"
|
||
type="danger"
|
||
size="small"
|
||
link
|
||
@click="handleUnbindCard(row)"
|
||
>
|
||
解绑
|
||
</ElButton>
|
||
</template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
<div
|
||
v-if="deviceCards.length === 0"
|
||
style="padding: 20px; color: #909399; text-align: center"
|
||
>
|
||
暂无设备绑定的卡
|
||
</div>
|
||
</div>
|
||
</ElDialog>
|
||
|
||
<!-- 切换SIM卡对话框 -->
|
||
<SwitchCardDialog
|
||
v-model="switchCardDialogVisible"
|
||
:device-info="currentOperatingImei"
|
||
:cards="deviceBindingCards"
|
||
:loading="loadingDeviceCards"
|
||
:confirm-loading="switchCardLoading"
|
||
@confirm="handleConfirmSwitchCard"
|
||
/>
|
||
|
||
<!-- 设置切卡模式对话框 -->
|
||
<SwitchModeDialog
|
||
v-model="switchModeDialogVisible"
|
||
:device-identifier="currentOperatingDeviceIdentifier"
|
||
:current-mode="currentDeviceSwitchMode"
|
||
:cards="switchModeCards"
|
||
:has-imei="currentOperatingDeviceHasImei"
|
||
:confirm-loading="switchModeLoading"
|
||
@confirm="handleConfirmSwitchMode"
|
||
/>
|
||
|
||
<!-- 实名认证策略对话框 -->
|
||
<RealnamePolicyDialog
|
||
v-model="realnamePolicyDialogVisible"
|
||
:asset-identifier="currentRealnamePolicyAsset"
|
||
:current-policy="currentRealnamePolicy"
|
||
@confirm="handleConfirmRealnamePolicy"
|
||
/>
|
||
<BatchRealnamePolicyDialog
|
||
v-model="batchRealnamePolicyDialogVisible"
|
||
:selected-count="selectedDevices.length"
|
||
asset-unit="台设备"
|
||
:loading="batchRealnamePolicyLoading"
|
||
:error-message="batchRealnamePolicyError"
|
||
device-policy-hint
|
||
@confirm="handleConfirmBatchRealnamePolicy"
|
||
/>
|
||
|
||
<!-- 操作审计日志弹窗 -->
|
||
<OperationLogsDialog
|
||
v-model="operationLogsDialogVisible"
|
||
:identifier="operationLogsIdentifier"
|
||
download-permission="device:download_log_file"
|
||
/>
|
||
<ElDialog
|
||
v-model="enterpriseDeviceAuthorizeDialogVisible"
|
||
title="授权设备给企业"
|
||
width="620px"
|
||
@close="handleEnterpriseDeviceAuthorizeDialogClose"
|
||
>
|
||
<ElForm
|
||
ref="enterpriseDeviceAuthorizeFormRef"
|
||
:model="enterpriseDeviceAuthorizeForm"
|
||
:rules="enterpriseDeviceAuthorizeRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="授权企业" prop="enterprise_id">
|
||
<ElSelect
|
||
v-model="enterpriseDeviceAuthorizeForm.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="enterpriseDeviceAuthorizeForm.selection_type">
|
||
<ElRadio label="list">设备号列表</ElRadio>
|
||
<ElRadio label="filter">筛选条件</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.LIST"
|
||
label="设备号列表"
|
||
>
|
||
<div>已选择 {{ selectedDevices.length }} 台设备</div>
|
||
</ElFormItem>
|
||
<template
|
||
v-if="enterpriseDeviceAuthorizeForm.selection_type === DeviceSelectionType.FILTER"
|
||
>
|
||
<ElFormItem label="设备号">
|
||
<ElInput
|
||
v-model="enterpriseDeviceAuthorizeForm.virtual_no"
|
||
placeholder="请输入设备号"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="批次号">
|
||
<ElSelect
|
||
v-model="enterpriseDeviceAuthorizeForm.batch_no"
|
||
placeholder="请选择或搜索批次号"
|
||
clearable
|
||
filterable
|
||
remote
|
||
:remote-method="handleSearchBatchNo"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="item in searchBatchNoOptions"
|
||
:key="item.id"
|
||
:label="item.batch_no"
|
||
:value="item.batch_no"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="店铺名称">
|
||
<ElCascader
|
||
v-model="enterpriseDeviceAuthorizeForm.shop_id_path"
|
||
:options="shopCascadeOptions"
|
||
:props="shopCascadeProps"
|
||
placeholder="请选择店铺名称"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
</template>
|
||
<ElFormItem label="备注">
|
||
<ElInput
|
||
v-model="enterpriseDeviceAuthorizeForm.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入备注信息"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="enterpriseDeviceAuthorizeDialogVisible = false">取消</ElButton>
|
||
<ElButton
|
||
type="primary"
|
||
@click="handleEnterpriseDeviceAuthorize"
|
||
:loading="enterpriseDeviceAuthorizeLoading"
|
||
>
|
||
确认授权
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
<ElDialog
|
||
v-model="enterpriseDeviceRecallDialogVisible"
|
||
title="撤销设备授权"
|
||
width="620px"
|
||
@close="handleEnterpriseDeviceRecallDialogClose"
|
||
>
|
||
<ElForm
|
||
ref="enterpriseDeviceRecallFormRef"
|
||
:model="enterpriseDeviceRecallForm"
|
||
:rules="enterpriseDeviceRecallRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="授权企业" prop="enterprise_id">
|
||
<ElSelect
|
||
v-model="enterpriseDeviceRecallForm.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="enterpriseDeviceRecallForm.selection_type">
|
||
<ElRadio label="list">设备号列表</ElRadio>
|
||
<ElRadio label="filter">筛选条件</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
<ElFormItem
|
||
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.LIST"
|
||
label="设备号列表"
|
||
>
|
||
<div>已选择 {{ selectedDevices.length }} 台设备</div>
|
||
</ElFormItem>
|
||
<template
|
||
v-if="enterpriseDeviceRecallForm.selection_type === DeviceSelectionType.FILTER"
|
||
>
|
||
<ElFormItem label="设备号">
|
||
<ElInput
|
||
v-model="enterpriseDeviceRecallForm.virtual_no"
|
||
placeholder="请输入设备号"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="批次号">
|
||
<ElSelect
|
||
v-model="enterpriseDeviceRecallForm.batch_no"
|
||
placeholder="请选择或搜索批次号"
|
||
clearable
|
||
filterable
|
||
remote
|
||
:remote-method="handleSearchBatchNo"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption
|
||
v-for="item in searchBatchNoOptions"
|
||
:key="item.id"
|
||
:label="item.batch_no"
|
||
:value="item.batch_no"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</template>
|
||
</ElForm>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="enterpriseDeviceRecallDialogVisible = false">取消</ElButton>
|
||
<ElButton
|
||
type="primary"
|
||
@click="handleEnterpriseDeviceRecall"
|
||
:loading="enterpriseDeviceRecallLoading"
|
||
>
|
||
确认撤销
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
<ElDialog
|
||
v-model="enterpriseDeviceResultDialogVisible"
|
||
:title="enterpriseDeviceResultTitle"
|
||
width="700px"
|
||
>
|
||
<ElDescriptions :column="2" border>
|
||
<ElDescriptionsItem label="成功数">
|
||
<ElTag type="success">{{ enterpriseDeviceOperationResult.success_count }}</ElTag>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="失败数">
|
||
<ElTag type="danger">{{ enterpriseDeviceOperationResult.fail_count }}</ElTag>
|
||
</ElDescriptionsItem>
|
||
</ElDescriptions>
|
||
<div
|
||
v-if="
|
||
enterpriseDeviceOperationResult.failed_items &&
|
||
enterpriseDeviceOperationResult.failed_items.length > 0
|
||
"
|
||
style="margin-top: 20px"
|
||
>
|
||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||
<ElTable :data="enterpriseDeviceOperationResult.failed_items" border max-height="300">
|
||
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
|
||
<ElTableColumn prop="reason" label="失败原因" />
|
||
</ElTable>
|
||
</div>
|
||
<div
|
||
v-if="
|
||
enterpriseDeviceOperationResult.authorized_devices &&
|
||
enterpriseDeviceOperationResult.authorized_devices.length > 0
|
||
"
|
||
style="margin-top: 20px"
|
||
>
|
||
<ElDivider content-position="left">已授权设备</ElDivider>
|
||
<ElTable
|
||
:data="enterpriseDeviceOperationResult.authorized_devices"
|
||
border
|
||
max-height="220"
|
||
>
|
||
<ElTableColumn prop="virtual_no" label="设备号" width="180" />
|
||
<ElTableColumn prop="card_count" label="绑定卡数" />
|
||
</ElTable>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton type="primary" @click="enterpriseDeviceResultDialogVisible = false"
|
||
>确定</ElButton
|
||
>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
<ExportTaskCreateDialog
|
||
v-model="exportDialogVisible"
|
||
scene="device"
|
||
:query="exportQuery"
|
||
confirm-permission="devices:export"
|
||
title="导出设备"
|
||
/>
|
||
</ElCard>
|
||
</div>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, h } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { RoutesAlias } from '@/router/routesAlias'
|
||
import {
|
||
DeviceService,
|
||
ShopService,
|
||
CardService,
|
||
PackageSeriesService,
|
||
AssetService,
|
||
EnterpriseService
|
||
} from '@/api/modules'
|
||
import { DeviceSelectionType } from '@/types/api'
|
||
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
|
||
import { Loading } from '@element-plus/icons-vue'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import SwitchCardDialog from '@/components/device/SwitchCardDialog.vue'
|
||
import SwitchModeDialog from '@/views/asset-management/asset-information/components/dialogs/SwitchModeDialog.vue'
|
||
import type {
|
||
Device,
|
||
DeviceStatus,
|
||
AllocateDevicesResponse,
|
||
RecallDevicesResponse,
|
||
BatchSetDeviceSeriesBindingRequest,
|
||
BatchSetDeviceSeriesBindingResponse
|
||
} from '@/types/api'
|
||
import type { SearchFormItem } from '@/types'
|
||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
|
||
import type { PackageSeriesResponse } from '@/types/api'
|
||
import type { EnterpriseItem } from '@/types/api/enterprise'
|
||
import type {
|
||
EnterpriseAllocateDevicesResponse,
|
||
EnterpriseRecallDevicesResponse
|
||
} from '@/types/api/enterpriseDevice'
|
||
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
|
||
import BatchRealnamePolicyDialog from '@/components/business/BatchRealnamePolicyDialog.vue'
|
||
import OperationLogsDialog from '@/components/business/OperationLogsDialog.vue'
|
||
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
|
||
import type { AssetRealnamePolicy } from '@/types/api'
|
||
|
||
defineOptions({ name: 'DeviceList' })
|
||
|
||
interface SwitchModeCardOption {
|
||
iot_card_id: number
|
||
iccid: string
|
||
slot_position?: number
|
||
carrier_name?: string
|
||
msisdn?: string
|
||
network_status?: number
|
||
real_name_status?: number
|
||
is_current?: boolean
|
||
}
|
||
|
||
const { hasAuth } = useAuth()
|
||
const router = useRouter()
|
||
|
||
const loading = ref(false)
|
||
const allocateLoading = ref(false)
|
||
const recallLoading = ref(false)
|
||
const tableRef = ref()
|
||
const allocateFormRef = ref<FormInstance>()
|
||
const recallFormRef = ref<FormInstance>()
|
||
const allocateDialogVisible = ref(false)
|
||
const recallDialogVisible = ref(false)
|
||
const selectedDevices = ref<Device[]>([])
|
||
const batchRealnamePolicyDialogVisible = ref(false)
|
||
const batchRealnamePolicyLoading = ref(false)
|
||
const batchRealnamePolicyError = ref('')
|
||
const operationLogsDialogVisible = ref(false)
|
||
const operationLogsIdentifier = ref('')
|
||
const exportDialogVisible = ref(false)
|
||
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 seriesBindingShopCascadeOptions = ref<any[]>([])
|
||
const seriesBindingShopCascadeProps = {
|
||
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,
|
||
exclude_self: true
|
||
})
|
||
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 allocateResult = ref<AllocateDevicesResponse | null>(null)
|
||
const recallResult = ref<RecallDevicesResponse | null>(null)
|
||
const enterpriseDeviceAuthorizeDialogVisible = ref(false)
|
||
const enterpriseDeviceRecallDialogVisible = ref(false)
|
||
const enterpriseDeviceResultDialogVisible = ref(false)
|
||
const enterpriseDeviceAuthorizeLoading = ref(false)
|
||
const enterpriseDeviceRecallLoading = ref(false)
|
||
const enterpriseDeviceResultTitle = ref('')
|
||
const enterpriseDeviceAuthorizeFormRef = ref<FormInstance>()
|
||
const enterpriseDeviceRecallFormRef = ref<FormInstance>()
|
||
const enterpriseOptions = ref<EnterpriseItem[]>([])
|
||
const enterpriseLoading = ref(false)
|
||
const enterpriseDeviceOperationResult = ref<
|
||
Partial<EnterpriseAllocateDevicesResponse & EnterpriseRecallDevicesResponse> & {
|
||
success_count: number
|
||
fail_count: number
|
||
}
|
||
>({
|
||
success_count: 0,
|
||
fail_count: 0,
|
||
failed_items: null,
|
||
authorized_devices: null
|
||
})
|
||
|
||
// 套餐系列绑定相关
|
||
const seriesBindingDialogVisible = ref(false)
|
||
const seriesBindingLoading = ref(false)
|
||
const seriesBindingFormRef = ref<FormInstance>()
|
||
const seriesLoading = ref(false)
|
||
const packageSeriesList = ref<PackageSeriesResponse[]>([])
|
||
const createInitialSeriesBindingState = () => ({
|
||
selection_type: DeviceSelectionType.LIST,
|
||
series_id: undefined as number | undefined,
|
||
virtual_no_start: '',
|
||
virtual_no_end: '',
|
||
virtual_no: '',
|
||
device_name: '',
|
||
device_type: '',
|
||
manufacturer: '',
|
||
batch_no: '',
|
||
status: undefined as DeviceStatus | undefined,
|
||
shop_id_path: undefined as number[] | undefined,
|
||
filter_series_id: undefined as number | undefined,
|
||
created_at_range: [] as string[],
|
||
created_at_start: '',
|
||
created_at_end: ''
|
||
})
|
||
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
||
const createInitialEnterpriseDeviceAuthorizeState = () => ({
|
||
enterprise_id: undefined as number | undefined,
|
||
selection_type: DeviceSelectionType.LIST,
|
||
virtual_no: '',
|
||
batch_no: '',
|
||
shop_id_path: undefined as number[] | undefined,
|
||
remark: ''
|
||
})
|
||
const createInitialEnterpriseDeviceRecallState = () => ({
|
||
enterprise_id: undefined as number | undefined,
|
||
selection_type: DeviceSelectionType.LIST,
|
||
virtual_no: '',
|
||
batch_no: ''
|
||
})
|
||
const enterpriseDeviceAuthorizeForm = reactive(createInitialEnterpriseDeviceAuthorizeState())
|
||
const enterpriseDeviceRecallForm = reactive(createInitialEnterpriseDeviceRecallState())
|
||
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 parseDeviceRangeEndpoint = (value: string): { prefix: string; number: bigint } | null => {
|
||
const normalizedValue = value.trim()
|
||
if (!normalizedValue) {
|
||
return null
|
||
}
|
||
|
||
const numericValue = parseNumericStringToBigInt(normalizedValue)
|
||
if (numericValue !== null) {
|
||
return {
|
||
prefix: '',
|
||
number: numericValue
|
||
}
|
||
}
|
||
|
||
const match = normalizedValue.match(/^(.*?)(\d+)$/)
|
||
if (!match) {
|
||
return null
|
||
}
|
||
|
||
try {
|
||
return {
|
||
prefix: match[1],
|
||
number: BigInt(match[2])
|
||
}
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
const getDeviceRangeCountText = (startValue: string, endValue: string): string => {
|
||
const start = parseDeviceRangeEndpoint(startValue)
|
||
const end = parseDeviceRangeEndpoint(endValue)
|
||
if (!start || !end || start.prefix !== end.prefix) {
|
||
return ''
|
||
}
|
||
|
||
const countText = getPositiveRangeCountText(start.number, end.number)
|
||
return countText ? formatDigitGrouping(countText) : ''
|
||
}
|
||
const seriesBindingRangeCountText = computed(() =>
|
||
getDeviceRangeCountText(seriesBindingForm.virtual_no_start, seriesBindingForm.virtual_no_end)
|
||
)
|
||
const seriesBindingRules = reactive<FormRules>({
|
||
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }],
|
||
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||
virtual_no_start: [
|
||
{
|
||
required: true,
|
||
validator: (_rule, value, callback) => {
|
||
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE && !value) {
|
||
callback(new Error('请输入起始设备号'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
virtual_no_end: [
|
||
{
|
||
required: true,
|
||
validator: (_rule, value, callback) => {
|
||
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE && !value) {
|
||
callback(new Error('请输入结束设备号'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
})
|
||
const createEnterpriseDeviceRules = () =>
|
||
reactive<FormRules>({
|
||
enterprise_id: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
||
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }]
|
||
})
|
||
const enterpriseDeviceAuthorizeRules = createEnterpriseDeviceRules()
|
||
const enterpriseDeviceRecallRules = createEnterpriseDeviceRules()
|
||
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
||
|
||
// 搜索表单店铺/系列选项
|
||
const shopOptions = ref<any[]>([])
|
||
const searchSeriesOptions = ref<any[]>([])
|
||
const searchBatchNoOptions = ref<any[]>([])
|
||
|
||
// 设备详情弹窗相关
|
||
const deviceDetailDialogVisible = ref(false)
|
||
const deviceDetailLoading = ref(false)
|
||
const currentDeviceDetail = ref<any>(null)
|
||
const deviceCards = ref<any[]>([])
|
||
const deviceCardsLoading = ref(false)
|
||
const deviceCardsDialogVisible = ref(false)
|
||
|
||
// 绑定卡相关
|
||
const bindCardLoading = ref(false)
|
||
const bindCardFormRef = ref<FormInstance>()
|
||
const iotCardList = ref<any[]>([])
|
||
const iotCardSearchLoading = ref(false)
|
||
const iotCardHasSearched = ref(false) // 标记是否已执行过搜索
|
||
const bindCardForm = reactive({
|
||
search_type: 'iccid' as 'iccid' | 'carrier_name',
|
||
iot_card_id: undefined as number | undefined,
|
||
slot_position: 1
|
||
})
|
||
const bindCardRules = reactive<FormRules>({
|
||
search_type: [{ required: true, message: '请选择搜索类型', trigger: 'change' }],
|
||
iot_card_id: [{ required: true, message: '请选择ICCID', trigger: 'change' }],
|
||
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
|
||
})
|
||
|
||
// 设备操作相关对话框
|
||
|
||
const currentOperatingImei = ref<string>('') // 用于存储当前操作设备的IMEI
|
||
const currentOperatingDevice = ref<Device | null>(null)
|
||
|
||
const switchCardDialogVisible = ref(false)
|
||
const switchCardLoading = ref(false)
|
||
const deviceBindingCards = ref<any[]>([]) // 设备绑定的卡列表
|
||
const loadingDeviceCards = ref(false) // 加载设备绑定卡列表的状态
|
||
|
||
// 切换模式相关
|
||
const switchModeDialogVisible = ref(false)
|
||
const switchModeLoading = ref(false)
|
||
const switchModeCards = ref<SwitchModeCardOption[]>([])
|
||
const currentOperatingDeviceIdentifier = ref<string>('')
|
||
const currentOperatingDeviceHasImei = ref(false)
|
||
const currentDeviceSwitchMode = ref<number>(0)
|
||
|
||
// 实名认证策略相关
|
||
const realnamePolicyDialogVisible = ref(false)
|
||
const currentRealnamePolicyAsset = ref<string>('')
|
||
const currentRealnamePolicy = ref<string>('none')
|
||
|
||
// 搜索表单初始值
|
||
const initialSearchState = {
|
||
virtual_no: '',
|
||
imei: '',
|
||
device_name: '',
|
||
status: undefined as DeviceStatus | undefined,
|
||
activation_status: undefined as number | undefined,
|
||
real_name_status: undefined as 0 | 1 | undefined,
|
||
batch_no: '',
|
||
device_type: '',
|
||
manufacturer: '',
|
||
shop_id: undefined as number | undefined,
|
||
series_id: undefined as number | undefined,
|
||
has_active_package: undefined as boolean | undefined,
|
||
authorized_enterprise_id: undefined as number | undefined,
|
||
is_authorized_to_enterprise: undefined as boolean | undefined,
|
||
dateRange: [] as string[],
|
||
created_at_start: '',
|
||
created_at_end: ''
|
||
}
|
||
|
||
// 搜索表单
|
||
const searchForm = reactive<Record<string, any>>({ ...initialSearchState })
|
||
|
||
// 搜索批次号(用于搜索表单)
|
||
const loadSearchBatchNoOptions = 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) {
|
||
searchBatchNoOptions.value = res.data.items || []
|
||
}
|
||
} catch (error) {
|
||
console.error('加载批次号选项失败:', error)
|
||
}
|
||
}
|
||
|
||
// 搜索批次号
|
||
const handleSearchBatchNo = (query: string) => {
|
||
if (query) {
|
||
loadSearchBatchNoOptions(query)
|
||
} else {
|
||
loadSearchBatchNoOptions()
|
||
}
|
||
}
|
||
|
||
// 搜索表单配置
|
||
const searchFormItems: SearchFormItem[] = [
|
||
{
|
||
label: '设备号',
|
||
prop: 'virtual_no',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入设备号'
|
||
}
|
||
},
|
||
{
|
||
label: '设备名称',
|
||
prop: 'device_name',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入设备名称'
|
||
}
|
||
},
|
||
{
|
||
label: 'IMEI',
|
||
prop: 'imei',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入IMEI'
|
||
}
|
||
},
|
||
{
|
||
label: '虚拟号',
|
||
prop: 'virtual_no',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入虚拟号'
|
||
}
|
||
},
|
||
{
|
||
label: '状态',
|
||
prop: 'status',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请选择状态'
|
||
},
|
||
options: () => [
|
||
{ label: '在库', value: 1 },
|
||
{ label: '已分销', value: 2 },
|
||
{ label: '已激活', value: 3 },
|
||
{ label: '已停用', value: 4 }
|
||
]
|
||
},
|
||
{
|
||
label: '激活状态',
|
||
prop: 'activation_status',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请选择激活状态'
|
||
},
|
||
options: () => [
|
||
{ label: '未激活', value: 0 },
|
||
{ label: '已激活', value: 1 }
|
||
]
|
||
},
|
||
{
|
||
label: '实名状态',
|
||
prop: 'real_name_status',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '全部'
|
||
},
|
||
options: () => [
|
||
{ label: '已实名', value: 1 },
|
||
{ label: '未实名', value: 0 }
|
||
]
|
||
},
|
||
{
|
||
label: '批次号',
|
||
prop: 'batch_no',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
filterable: true,
|
||
remote: true,
|
||
remoteMethod: handleSearchBatchNo,
|
||
loading: false,
|
||
placeholder: '请选择或搜索批次号'
|
||
},
|
||
options: () =>
|
||
searchBatchNoOptions.value.map((b) => ({
|
||
label: b.batch_no,
|
||
value: b.batch_no
|
||
}))
|
||
},
|
||
{
|
||
label: '设备类型',
|
||
prop: 'device_type',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入设备类型'
|
||
}
|
||
},
|
||
{
|
||
label: '制造商',
|
||
prop: 'manufacturer',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入制造商'
|
||
}
|
||
},
|
||
{
|
||
label: '店铺名称',
|
||
prop: 'shop_id',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
filterable: true,
|
||
remote: true,
|
||
remoteMethod: (query: string) => searchShops(query),
|
||
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: (query: string) => searchSeries(query),
|
||
loading: seriesLoading.value,
|
||
placeholder: '请选择或搜索套餐系列'
|
||
},
|
||
options: () =>
|
||
searchSeriesOptions.value.map((s) => ({
|
||
label: s.series_name,
|
||
value: s.id
|
||
}))
|
||
},
|
||
{
|
||
label: '起止时间',
|
||
prop: 'dateRange',
|
||
type: 'date',
|
||
config: {
|
||
type: 'daterange',
|
||
rangeSeparator: '至',
|
||
startPlaceholder: '开始时间',
|
||
endPlaceholder: '结束时间',
|
||
valueFormat: 'YYYY-MM-DD'
|
||
}
|
||
}
|
||
]
|
||
|
||
// 分页
|
||
searchFormItems.splice(searchFormItems.length - 1, 0, {
|
||
label: '有效套餐',
|
||
prop: 'has_active_package',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '全部'
|
||
},
|
||
options: () => [
|
||
{ label: '是', value: true },
|
||
{ label: '否', value: false }
|
||
]
|
||
})
|
||
searchFormItems.splice(searchFormItems.length - 1, 0, {
|
||
label: '授权企业',
|
||
prop: 'authorized_enterprise_id',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
filterable: true,
|
||
remote: true,
|
||
remoteMethod: (query: string) => handleSearchEnterprise(query),
|
||
loading: enterpriseLoading.value,
|
||
placeholder: '请选择或搜索企业'
|
||
},
|
||
options: () =>
|
||
enterpriseOptions.value.map((enterprise) => ({
|
||
label: enterprise.enterprise_name,
|
||
value: enterprise.id
|
||
}))
|
||
})
|
||
searchFormItems.splice(searchFormItems.length - 1, 0, {
|
||
label: '授权状态',
|
||
prop: 'is_authorized_to_enterprise',
|
||
type: 'select',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '全部'
|
||
},
|
||
options: () => [
|
||
{ label: '已授权', value: true },
|
||
{ label: '未授权', value: false }
|
||
]
|
||
})
|
||
|
||
const pagination = reactive({
|
||
page: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 列配置
|
||
const columnOptions = [
|
||
{ label: '设备号', prop: 'virtual_no' },
|
||
{ label: 'IMEI', prop: 'imei' },
|
||
{ label: '设备名称', prop: 'device_name' },
|
||
{ label: '店铺名称', prop: 'shop_name' },
|
||
{ label: '套餐系列', prop: 'series_name' },
|
||
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
||
{ label: '设备型号', prop: 'device_model' },
|
||
{ label: '设备类型', prop: 'device_type' },
|
||
{ label: '状态', prop: 'status' },
|
||
{ label: '状态名称', prop: 'status_name' },
|
||
{ label: '资产世代', prop: 'generation' },
|
||
{ label: '业务状态', prop: 'asset_status' },
|
||
{ label: '激活状态', prop: 'activation_status_name' },
|
||
{ label: '实名状态', prop: 'real_name_status_name' },
|
||
{ label: '在线状态', prop: 'online_status' },
|
||
{ label: '实名策略', prop: 'realname_policy' },
|
||
{ label: '切卡模式', prop: 'switch_mode' },
|
||
{ label: '激活时间', prop: 'activated_at' },
|
||
{ label: '制造商', prop: 'manufacturer' },
|
||
{ label: '软件版本', prop: 'software_version' },
|
||
{ label: '最大插槽数', prop: 'max_sim_slots' },
|
||
{ label: '已绑定卡数', prop: 'bound_card_count' },
|
||
{ label: '序列号', prop: 'sn' },
|
||
{ label: '批次号', prop: 'batch_no' },
|
||
{ label: '最后在线时间', prop: 'last_online_time' },
|
||
{ label: '最后同步时间', prop: 'last_gateway_sync_at' },
|
||
{ label: '创建时间', prop: 'created_at' },
|
||
{ label: '操作', prop: 'operation' }
|
||
]
|
||
|
||
const deviceList = ref<Device[]>([])
|
||
|
||
// 分配表单
|
||
const allocateForm = reactive({
|
||
target_shop_id: undefined as number[] | undefined,
|
||
remark: ''
|
||
})
|
||
|
||
// 分配表单验证规则
|
||
const allocateRules = reactive<FormRules>({
|
||
target_shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
|
||
})
|
||
|
||
// 回收表单
|
||
const recallForm = reactive({
|
||
remark: ''
|
||
})
|
||
|
||
// 跳转到资产信息页面(原设备详情)
|
||
const goToDeviceSearchDetail = (deviceNo: string) => {
|
||
if (hasAuth('device:view_detail')) {
|
||
router.push({
|
||
path: RoutesAlias.AssetInformation,
|
||
query: {
|
||
virtual_no: deviceNo
|
||
}
|
||
})
|
||
} else {
|
||
ElMessage.warning('您没有查看详情的权限')
|
||
}
|
||
}
|
||
|
||
// 查看设备设备绑定的卡
|
||
const handleViewCards = async (device: Device) => {
|
||
currentDeviceDetail.value = device
|
||
deviceCards.value = []
|
||
deviceCardsDialogVisible.value = true
|
||
// 重置绑定卡表单
|
||
bindCardForm.search_type = 'iccid'
|
||
bindCardForm.iot_card_id = undefined
|
||
bindCardForm.slot_position = 1
|
||
bindCardFormRef.value?.resetFields()
|
||
// 加载未绑定设备的卡列表
|
||
await Promise.all([loadDeviceCards(device.virtual_no), loadDefaultIotCards()])
|
||
}
|
||
|
||
// 加载设备绑定的卡列表
|
||
const loadDeviceCards = async (virtualNo: string) => {
|
||
deviceCardsLoading.value = true
|
||
try {
|
||
const res = await DeviceService.getDeviceCards(virtualNo)
|
||
if (res.code === 0 && res.data) {
|
||
deviceCards.value = (res.data.bindings || []).sort(
|
||
(a, b) => a.slot_position - b.slot_position
|
||
)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取设备绑定的卡列表失败:', error)
|
||
} finally {
|
||
deviceCardsLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载默认的IoT卡列表
|
||
const loadDefaultIotCards = async () => {
|
||
iotCardSearchLoading.value = true
|
||
try {
|
||
const res = await CardService.getStandaloneIotCards({
|
||
page: 1,
|
||
page_size: 20,
|
||
is_standalone: true
|
||
})
|
||
if (res.code === 0 && res.data?.items) {
|
||
iotCardList.value = res.data.items
|
||
}
|
||
} catch (error) {
|
||
console.error('获取IoT卡列表失败:', error)
|
||
} finally {
|
||
iotCardSearchLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 搜索IoT卡(根据ICCID或运营商名称)
|
||
const searchIotCards = async (query: string) => {
|
||
if (!query) {
|
||
await loadDefaultIotCards()
|
||
return
|
||
}
|
||
iotCardSearchLoading.value = true
|
||
iotCardHasSearched.value = true
|
||
try {
|
||
const params: any = {
|
||
page: 1,
|
||
page_size: 20,
|
||
is_standalone: true
|
||
}
|
||
if (bindCardForm.search_type === 'iccid') {
|
||
params.iccid = query
|
||
} else if (bindCardForm.search_type === 'carrier_name') {
|
||
params.carrier_name = query
|
||
}
|
||
const res = await CardService.getStandaloneIotCards(params)
|
||
if (res.code === 0 && res.data?.items) {
|
||
iotCardList.value = res.data.items
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索IoT卡失败:', error)
|
||
} finally {
|
||
iotCardSearchLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 确认绑定卡
|
||
const handleConfirmBindCard = async () => {
|
||
if (!bindCardFormRef.value || !currentDeviceDetail.value) return
|
||
|
||
await bindCardFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
bindCardLoading.value = true
|
||
try {
|
||
const res = await DeviceService.bindCard(currentDeviceDetail.value.virtual_no, {
|
||
iot_card_id: bindCardForm.iot_card_id!,
|
||
slot_position: bindCardForm.slot_position
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage.success('绑定成功')
|
||
// 重置表单
|
||
bindCardForm.iot_card_id = undefined
|
||
bindCardForm.slot_position = 1
|
||
bindCardFormRef.value?.resetFields()
|
||
// 重新加载卡列表
|
||
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
|
||
// 刷新设备详情以更新绑定卡数量
|
||
const detailRes = await AssetService.resolveAsset(currentDeviceDetail.value.virtual_no)
|
||
if (detailRes.code === 0 && detailRes.data) {
|
||
currentDeviceDetail.value = detailRes.data
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('绑定卡失败:', error)
|
||
} finally {
|
||
bindCardLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 解绑卡
|
||
const handleUnbindCard = (row: any) => {
|
||
ElMessageBox.confirm(`确定要解绑 ICCID: ${row.iccid} 吗?`, '解绑确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
const res = await DeviceService.unbindCard(
|
||
currentDeviceDetail.value.virtual_no,
|
||
row.iccid
|
||
)
|
||
if (res.code === 0) {
|
||
ElMessage.success('解绑成功')
|
||
// 重新加载卡列表
|
||
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
|
||
// 刷新设备详情以更新绑定卡数量
|
||
const detailRes = await AssetService.resolveAsset(currentDeviceDetail.value.virtual_no)
|
||
if (detailRes.code === 0 && detailRes.data) {
|
||
currentDeviceDetail.value = detailRes.data
|
||
}
|
||
} else {
|
||
ElMessage.error(res.msg || '解绑失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('解绑卡失败:', error)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 获取网络状态标签类型
|
||
const getNetworkStatusTagType = (networkStatus: number) => {
|
||
// 0: 停机 (danger 红色)
|
||
// 1: 开机 - 正常/异常 (success 绿色 / warning 黄色)
|
||
if (networkStatus === 0) {
|
||
return 'danger' // 停机 - 红色
|
||
} else if (networkStatus === 1) {
|
||
return 'success' // 开机正常 - 绿色
|
||
}
|
||
return 'info' // 未知状态
|
||
}
|
||
|
||
// 获取网络状态文本
|
||
const getNetworkStatusText = (networkStatus: number) => {
|
||
// 0: 停机
|
||
// 1: 开机 - 显示为"正常"
|
||
const textMap: Record<number, string> = {
|
||
0: '停机',
|
||
1: '正常' // 开机显示为"正常"
|
||
}
|
||
return textMap[networkStatus] || '未知'
|
||
}
|
||
|
||
// 获取设备状态标签类型
|
||
const getDeviceStatusTagType = (status: number) => {
|
||
const typeMap: Record<number, any> = {
|
||
1: 'info', // 在库
|
||
2: 'warning', // 已分销
|
||
3: 'success', // 已激活
|
||
4: 'danger' // 已停用
|
||
}
|
||
return typeMap[status] || 'info'
|
||
}
|
||
|
||
// 动态列配置
|
||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||
{
|
||
prop: 'virtual_no',
|
||
label: '设备号',
|
||
minWidth: 170,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: Device) => {
|
||
return h(
|
||
'span',
|
||
{
|
||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||
onClick: (e: MouseEvent) => {
|
||
e.stopPropagation()
|
||
goToDeviceSearchDetail(row.virtual_no)
|
||
}
|
||
},
|
||
row.virtual_no
|
||
)
|
||
}
|
||
},
|
||
{
|
||
prop: 'imei',
|
||
label: 'IMEI',
|
||
minWidth: 150,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: Device) => row.imei || '-'
|
||
},
|
||
{
|
||
prop: 'device_name',
|
||
label: '设备名称',
|
||
showOverflowTooltip: true,
|
||
minWidth: 150
|
||
},
|
||
{
|
||
prop: 'shop_name',
|
||
label: '店铺名称',
|
||
minWidth: 120
|
||
},
|
||
{
|
||
prop: 'series_name',
|
||
label: '套餐系列',
|
||
minWidth: 180,
|
||
showOverflowTooltip: true
|
||
},
|
||
{
|
||
prop: 'estimated_final_expires_at',
|
||
label: '预计套餐到期时间',
|
||
width: 180,
|
||
formatter: (row: Device) =>
|
||
h(
|
||
'span',
|
||
{
|
||
class: { 'expiry-estimate--expiring': row.is_expiring },
|
||
title: getExpiryEstimateTooltip(row)
|
||
},
|
||
getExpiryEstimateText(row)
|
||
)
|
||
},
|
||
{
|
||
prop: 'device_model',
|
||
label: '设备型号',
|
||
minWidth: 120
|
||
},
|
||
{
|
||
prop: 'device_type',
|
||
label: '设备类型',
|
||
width: 120
|
||
},
|
||
{
|
||
prop: 'status',
|
||
label: '状态',
|
||
width: 100,
|
||
formatter: (row: Device) => {
|
||
const statusMap: Record<number, { text: string; type: any }> = {
|
||
1: { text: '在库', type: 'info' },
|
||
2: { text: '已分销', type: 'primary' },
|
||
3: { text: '已激活', type: 'success' },
|
||
4: { text: '已停用', type: 'danger' }
|
||
}
|
||
const status = statusMap[row.status] || { text: '未知', type: 'info' }
|
||
return h(ElTag, { type: status.type }, () => status.text)
|
||
}
|
||
},
|
||
{
|
||
prop: 'status_name',
|
||
label: '状态名称',
|
||
width: 100,
|
||
formatter: (row: Device) => row.status_name || '-'
|
||
},
|
||
{
|
||
prop: 'generation',
|
||
label: '资产世代',
|
||
width: 100,
|
||
formatter: (row: Device) => row.generation ?? '-'
|
||
},
|
||
{
|
||
prop: 'asset_status',
|
||
label: '业务状态',
|
||
width: 100,
|
||
formatter: (row: Device) => {
|
||
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_name',
|
||
label: '激活状态',
|
||
width: 120,
|
||
formatter: (row: Device) => row.activation_status_name || '-'
|
||
},
|
||
{
|
||
prop: 'real_name_status_name',
|
||
label: '实名状态',
|
||
width: 100,
|
||
formatter: (row: Device) => row.real_name_status_name || '-'
|
||
},
|
||
{
|
||
prop: 'online_status',
|
||
label: '在线状态',
|
||
width: 100,
|
||
formatter: (row: Device) => {
|
||
const onlineStatusMap: Record<number, { text: string; type: any }> = {
|
||
0: { text: '未知', type: 'info' },
|
||
1: { text: '在线', type: 'success' },
|
||
2: { text: '离线', type: 'danger' }
|
||
}
|
||
const status = onlineStatusMap[row.online_status ?? 0] || { text: '未知', type: 'info' }
|
||
return h(ElTag, { type: status.type }, () => status.text)
|
||
}
|
||
},
|
||
{
|
||
prop: 'realname_policy',
|
||
label: '实名策略',
|
||
width: 150,
|
||
formatter: (row: Device) => {
|
||
const policy = row.realname_policy || ''
|
||
const policyMap: Record<string, string> = {
|
||
none: '无需实名',
|
||
before_order: '先实名后充值/购买',
|
||
after_order: '先充值/购买后实名'
|
||
}
|
||
return policyMap[policy] || (policy ? policy : '未知')
|
||
}
|
||
},
|
||
{
|
||
prop: 'switch_mode',
|
||
label: '切卡模式',
|
||
width: 100,
|
||
formatter: (row: Device) => {
|
||
const modeMap: Record<string, string> = {
|
||
'0': '自动',
|
||
'1': '手动'
|
||
}
|
||
return modeMap[row.switch_mode || ''] || '-'
|
||
}
|
||
},
|
||
{
|
||
prop: 'activated_at',
|
||
label: '激活时间',
|
||
width: 180,
|
||
formatter: (row: Device) => (row.activated_at ? formatDateTime(row.activated_at) : '-')
|
||
},
|
||
{
|
||
prop: 'manufacturer',
|
||
label: '制造商',
|
||
minWidth: 100
|
||
},
|
||
{
|
||
prop: 'software_version',
|
||
label: '软件版本',
|
||
minWidth: 180,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: Device) => row.software_version || '-'
|
||
},
|
||
{
|
||
prop: 'max_sim_slots',
|
||
label: '最大插槽数',
|
||
width: 100
|
||
},
|
||
{
|
||
prop: 'bound_card_count',
|
||
label: '已绑定卡数',
|
||
width: 110,
|
||
formatter: (row: Device) => {
|
||
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
|
||
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
|
||
}
|
||
},
|
||
{
|
||
prop: 'sn',
|
||
label: '序列号',
|
||
minWidth: 120,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: Device) => row.sn || '-'
|
||
},
|
||
{
|
||
prop: 'batch_no',
|
||
label: '批次号',
|
||
minWidth: 180,
|
||
showOverflowTooltip: true,
|
||
formatter: (row: Device) => row.batch_no || '-'
|
||
},
|
||
{
|
||
prop: 'last_online_time',
|
||
label: '最后在线时间',
|
||
width: 180,
|
||
formatter: (row: Device) =>
|
||
row.last_online_time ? formatDateTime(row.last_online_time) : '-'
|
||
},
|
||
{
|
||
prop: 'last_gateway_sync_at',
|
||
label: '最后同步时间',
|
||
width: 180,
|
||
formatter: (row: Device) =>
|
||
row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-'
|
||
},
|
||
{
|
||
prop: 'created_at',
|
||
label: '创建时间',
|
||
width: 180,
|
||
formatter: (row: Device) => formatDateTime(row.created_at)
|
||
}
|
||
])
|
||
|
||
// 搜索店铺(用于搜索表单)
|
||
const searchShops = 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 searchSeries = 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 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
|
||
}
|
||
}
|
||
|
||
let isFirstActivation = true
|
||
onMounted(() => {
|
||
getTableData()
|
||
})
|
||
|
||
// 当页面被 keep-alive 激活时自动刷新数据
|
||
onActivated(() => {
|
||
if (!isFirstActivation) {
|
||
getTableData()
|
||
}
|
||
isFirstActivation = false
|
||
})
|
||
|
||
// 获取设备列表
|
||
const getTableData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
page: pagination.page,
|
||
page_size: pagination.pageSize,
|
||
virtual_no: searchForm.virtual_no || undefined,
|
||
imei: searchForm.imei || undefined,
|
||
device_name: searchForm.device_name || undefined,
|
||
status: searchForm.status,
|
||
activation_status: searchForm.activation_status ?? undefined,
|
||
real_name_status: searchForm.real_name_status ?? undefined,
|
||
batch_no: searchForm.batch_no || undefined,
|
||
device_type: searchForm.device_type || undefined,
|
||
manufacturer: searchForm.manufacturer || undefined,
|
||
shop_id: searchForm.shop_id || undefined,
|
||
series_id: searchForm.series_id || undefined,
|
||
has_active_package: searchForm.has_active_package ?? undefined,
|
||
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
|
||
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
|
||
created_at_start: searchForm.created_at_start || undefined,
|
||
created_at_end: searchForm.created_at_end || undefined
|
||
}
|
||
const res = await DeviceService.getDevices(params)
|
||
if (res.code === 0 && res.data) {
|
||
deviceList.value = res.data.items || []
|
||
pagination.total = res.data.total || 0
|
||
}
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const exportQuery = computed(() => {
|
||
const query: Record<string, unknown> = {
|
||
virtual_no: searchForm.virtual_no || undefined,
|
||
imei: searchForm.imei || undefined,
|
||
device_name: searchForm.device_name || undefined,
|
||
status: searchForm.status,
|
||
activation_status: searchForm.activation_status ?? undefined,
|
||
real_name_status: searchForm.real_name_status ?? undefined,
|
||
batch_no: searchForm.batch_no || undefined,
|
||
device_type: searchForm.device_type || undefined,
|
||
manufacturer: searchForm.manufacturer || undefined,
|
||
shop_id: searchForm.shop_id || undefined,
|
||
series_id: searchForm.series_id || undefined,
|
||
has_active_package: searchForm.has_active_package ?? undefined,
|
||
authorized_enterprise_id: searchForm.authorized_enterprise_id || undefined,
|
||
is_authorized_to_enterprise: searchForm.is_authorized_to_enterprise ?? undefined,
|
||
created_at_start: searchForm.created_at_start || undefined,
|
||
created_at_end: searchForm.created_at_end || undefined
|
||
}
|
||
|
||
Object.keys(query).forEach((key) => {
|
||
if (query[key] === undefined || query[key] === null || query[key] === '') {
|
||
delete query[key]
|
||
}
|
||
})
|
||
|
||
return query
|
||
})
|
||
|
||
const showExportDialog = () => {
|
||
exportDialogVisible.value = true
|
||
}
|
||
|
||
// 重置搜索
|
||
const handleReset = () => {
|
||
Object.assign(searchForm, { ...initialSearchState })
|
||
pagination.page = 1
|
||
getTableData()
|
||
}
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
// 处理日期范围
|
||
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
|
||
searchForm.created_at_start = searchForm.dateRange[0] || ''
|
||
searchForm.created_at_end = searchForm.dateRange[1] || ''
|
||
} else {
|
||
searchForm.created_at_start = ''
|
||
searchForm.created_at_end = ''
|
||
}
|
||
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: Device[]) => {
|
||
selectedDevices.value = selection
|
||
}
|
||
|
||
const showBatchRealnamePolicyDialog = () => {
|
||
if (selectedDevices.value.length === 0) {
|
||
ElMessage.warning('请先选择要修改的设备')
|
||
return
|
||
}
|
||
|
||
batchRealnamePolicyError.value = ''
|
||
batchRealnamePolicyDialogVisible.value = true
|
||
}
|
||
|
||
const handleConfirmBatchRealnamePolicy = async (realnamePolicy: AssetRealnamePolicy) => {
|
||
const assetIds = selectedDevices.value.map((device) => device.id)
|
||
if (assetIds.length === 0) {
|
||
batchRealnamePolicyError.value = '请先选择要修改的设备'
|
||
return
|
||
}
|
||
if (assetIds.length > 500) {
|
||
batchRealnamePolicyError.value = '单次最多可修改500台设备'
|
||
return
|
||
}
|
||
|
||
batchRealnamePolicyError.value = ''
|
||
batchRealnamePolicyLoading.value = true
|
||
try {
|
||
const res = await DeviceService.batchUpdateRealnamePolicy(
|
||
{ asset_ids: assetIds, realname_policy: realnamePolicy },
|
||
{ requestOptions: { errorMessageMode: 'none' } }
|
||
)
|
||
if (res.code !== 0) {
|
||
batchRealnamePolicyError.value = res.msg || '批量修改实名顺序失败'
|
||
return
|
||
}
|
||
|
||
ElMessage.success('批量修改实名顺序成功')
|
||
batchRealnamePolicyDialogVisible.value = false
|
||
selectedDevices.value = []
|
||
await getTableData()
|
||
} catch (error: any) {
|
||
batchRealnamePolicyError.value = error?.response?.data?.msg || '批量修改实名顺序失败'
|
||
} finally {
|
||
batchRealnamePolicyLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 删除设备
|
||
const deleteDevice = (row: Device) => {
|
||
ElMessageBox.confirm(
|
||
`确定删除设备 ${row.virtual_no} 吗?删除后将自动解绑所有卡。`,
|
||
'删除确认',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'error'
|
||
}
|
||
)
|
||
.then(async () => {
|
||
try {
|
||
await DeviceService.deleteDevice(row.virtual_no)
|
||
ElMessage.success('删除成功')
|
||
await getTableData()
|
||
} catch (error) {
|
||
console.error(error)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 清除设备套餐系列绑定
|
||
const handleClearDeviceSeries = async (deviceNo: string) => {
|
||
const device = deviceList.value.find((d) => d.virtual_no === deviceNo)
|
||
if (!device) {
|
||
ElMessage.error('未找到该设备')
|
||
return
|
||
}
|
||
|
||
ElMessageBox.confirm(`确定要清除设备 ${deviceNo} 的套餐系列关联吗?`, '确认清除', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
const res = await DeviceService.batchSetDeviceSeriesBinding({
|
||
selection_type: DeviceSelectionType.LIST,
|
||
device_ids: [device.id],
|
||
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 handleBatchAllocate = async () => {
|
||
if (selectedDevices.value.length === 0) {
|
||
ElMessage.warning('请先选择要分配的设备')
|
||
return
|
||
}
|
||
allocateForm.target_shop_id = undefined
|
||
allocateForm.remark = ''
|
||
allocateResult.value = null
|
||
shopCascadeOptions.value = []
|
||
allocateDialogVisible.value = true
|
||
}
|
||
|
||
// 确认批量分配
|
||
const handleConfirmAllocate = async () => {
|
||
if (!allocateFormRef.value) return
|
||
|
||
await allocateFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
allocateLoading.value = true
|
||
try {
|
||
const targetShopPath = allocateForm.target_shop_id
|
||
if (!targetShopPath?.length) {
|
||
ElMessage.warning('请选择目标店铺')
|
||
return
|
||
}
|
||
|
||
const data = {
|
||
device_ids: selectedDevices.value.map((d) => d.id),
|
||
target_shop_id: targetShopPath[targetShopPath.length - 1],
|
||
remark: allocateForm.remark
|
||
}
|
||
const res = await DeviceService.allocateDevices(data)
|
||
if (res.code === 0) {
|
||
allocateResult.value = res.data
|
||
if (res.data.fail_count === 0) {
|
||
ElMessage.success('全部分配成功')
|
||
setTimeout(() => {
|
||
handleCloseAllocateDialog()
|
||
getTableData()
|
||
}, 1500)
|
||
} else {
|
||
ElMessage.warning(`部分分配失败,请查看失败详情`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
allocateLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 关闭分配对话框
|
||
const handleCloseAllocateDialog = () => {
|
||
allocateDialogVisible.value = false
|
||
allocateResult.value = null
|
||
if (allocateFormRef.value) {
|
||
allocateFormRef.value.resetFields()
|
||
}
|
||
}
|
||
|
||
// 批量回收
|
||
const handleBatchRecall = () => {
|
||
if (selectedDevices.value.length === 0) {
|
||
ElMessage.warning('请先选择要回收的设备')
|
||
return
|
||
}
|
||
recallForm.remark = ''
|
||
recallResult.value = null
|
||
recallDialogVisible.value = true
|
||
}
|
||
|
||
// 确认批量回收
|
||
const handleConfirmRecall = async () => {
|
||
recallLoading.value = true
|
||
try {
|
||
const data = {
|
||
device_ids: selectedDevices.value.map((d) => d.id),
|
||
remark: recallForm.remark
|
||
}
|
||
const res = await DeviceService.recallDevices(data)
|
||
if (res.code === 0) {
|
||
recallResult.value = res.data
|
||
if (res.data.fail_count === 0) {
|
||
ElMessage.success('全部回收成功')
|
||
setTimeout(() => {
|
||
handleCloseRecallDialog()
|
||
getTableData()
|
||
}, 1500)
|
||
} else {
|
||
ElMessage.warning(`部分回收失败,请查看失败详情`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
recallLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 关闭回收对话框
|
||
const handleCloseRecallDialog = () => {
|
||
recallDialogVisible.value = false
|
||
recallResult.value = null
|
||
recallForm.remark = ''
|
||
}
|
||
|
||
const getInferredDeviceEnterpriseId = () => {
|
||
const filterEnterpriseId = searchForm.authorized_enterprise_id
|
||
if (filterEnterpriseId) return filterEnterpriseId
|
||
|
||
const selectedEnterpriseIds = Array.from(
|
||
new Set(
|
||
selectedDevices.value
|
||
.map((device) => device.authorized_enterprise_id)
|
||
.filter((id): id is number => typeof id === 'number')
|
||
)
|
||
)
|
||
|
||
return selectedEnterpriseIds.length === 1 ? selectedEnterpriseIds[0] : undefined
|
||
}
|
||
|
||
const resetEnterpriseDeviceAuthorizeForm = () => {
|
||
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState(), {
|
||
selection_type:
|
||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
|
||
virtual_no: searchForm.virtual_no || '',
|
||
batch_no: searchForm.batch_no || '',
|
||
shop_id_path: undefined
|
||
})
|
||
}
|
||
|
||
const resetEnterpriseDeviceRecallForm = () => {
|
||
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState(), {
|
||
enterprise_id: getInferredDeviceEnterpriseId(),
|
||
selection_type:
|
||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER,
|
||
virtual_no: searchForm.virtual_no || '',
|
||
batch_no: searchForm.batch_no || ''
|
||
})
|
||
}
|
||
|
||
const showEnterpriseDeviceAuthorizeDialog = async () => {
|
||
resetEnterpriseDeviceAuthorizeForm()
|
||
await Promise.all([
|
||
handleSearchEnterprise(),
|
||
loadSearchBatchNoOptions(),
|
||
loadTopLevelShopCascadeOptions()
|
||
])
|
||
enterpriseDeviceAuthorizeDialogVisible.value = true
|
||
enterpriseDeviceAuthorizeFormRef.value?.clearValidate()
|
||
}
|
||
|
||
const showEnterpriseDeviceRecallDialog = async () => {
|
||
resetEnterpriseDeviceRecallForm()
|
||
await Promise.all([handleSearchEnterprise(), loadSearchBatchNoOptions()])
|
||
enterpriseDeviceRecallDialogVisible.value = true
|
||
enterpriseDeviceRecallFormRef.value?.clearValidate()
|
||
}
|
||
|
||
const handleEnterpriseDeviceAuthorizeDialogClose = () => {
|
||
Object.assign(enterpriseDeviceAuthorizeForm, createInitialEnterpriseDeviceAuthorizeState())
|
||
enterpriseDeviceAuthorizeFormRef.value?.resetFields()
|
||
}
|
||
|
||
const handleEnterpriseDeviceRecallDialogClose = () => {
|
||
Object.assign(enterpriseDeviceRecallForm, createInitialEnterpriseDeviceRecallState())
|
||
enterpriseDeviceRecallFormRef.value?.resetFields()
|
||
}
|
||
|
||
const buildEnterpriseDeviceParams = (form: any) => {
|
||
const params: Record<string, unknown> = {
|
||
selection_type: form.selection_type,
|
||
remark: form.remark || undefined
|
||
}
|
||
|
||
if (form.selection_type === DeviceSelectionType.LIST) {
|
||
const deviceNos = selectedDevices.value.map((device) => device.virtual_no).filter(Boolean)
|
||
if (!deviceNos.length) {
|
||
ElMessage.warning('列表模式下请先选择设备')
|
||
return null
|
||
}
|
||
params.device_nos = deviceNos
|
||
} else {
|
||
if (form.batch_no) params.batch_no = form.batch_no
|
||
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.virtual_no) params.virtual_no = form.virtual_no
|
||
}
|
||
|
||
Object.keys(params).forEach((key) => {
|
||
if (params[key] === undefined || params[key] === '') delete params[key]
|
||
})
|
||
|
||
return params
|
||
}
|
||
|
||
const applyEnterpriseDeviceOperationSuccess = async (
|
||
title: string,
|
||
result: EnterpriseAllocateDevicesResponse | EnterpriseRecallDevicesResponse
|
||
) => {
|
||
enterpriseDeviceOperationResult.value = result
|
||
enterpriseDeviceResultTitle.value = title
|
||
enterpriseDeviceResultDialogVisible.value = true
|
||
selectedDevices.value = []
|
||
await getTableData()
|
||
}
|
||
|
||
const handleEnterpriseDeviceAuthorize = async () => {
|
||
if (!enterpriseDeviceAuthorizeFormRef.value) return
|
||
|
||
await enterpriseDeviceAuthorizeFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
const params = buildEnterpriseDeviceParams(enterpriseDeviceAuthorizeForm)
|
||
if (!params) return
|
||
|
||
enterpriseDeviceAuthorizeLoading.value = true
|
||
try {
|
||
const res = await EnterpriseService.allocateDevices(
|
||
enterpriseDeviceAuthorizeForm.enterprise_id!,
|
||
params as any
|
||
)
|
||
if (res.code === 0) {
|
||
enterpriseDeviceAuthorizeDialogVisible.value = false
|
||
await applyEnterpriseDeviceOperationSuccess('授权设备给企业结果', res.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('授权设备给企业失败:', error)
|
||
} finally {
|
||
enterpriseDeviceAuthorizeLoading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleEnterpriseDeviceRecall = async () => {
|
||
if (!enterpriseDeviceRecallFormRef.value) return
|
||
|
||
await enterpriseDeviceRecallFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
const params = buildEnterpriseDeviceParams(enterpriseDeviceRecallForm)
|
||
if (!params) return
|
||
|
||
enterpriseDeviceRecallLoading.value = true
|
||
try {
|
||
const res = await EnterpriseService.recallDevices(
|
||
enterpriseDeviceRecallForm.enterprise_id!,
|
||
params as any
|
||
)
|
||
if (res.code === 0) {
|
||
enterpriseDeviceRecallDialogVisible.value = false
|
||
await applyEnterpriseDeviceOperationSuccess('撤销设备授权结果', res.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('撤销设备授权失败:', error)
|
||
} finally {
|
||
enterpriseDeviceRecallLoading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// 批量设置套餐系列
|
||
const handleBatchSetSeries = async () => {
|
||
seriesBindingResult.value = null
|
||
Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
|
||
selection_type:
|
||
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
|
||
})
|
||
await Promise.all([
|
||
loadPackageSeriesList(),
|
||
loadSearchBatchNoOptions(),
|
||
loadSeriesBindingShopCascadeOptions()
|
||
])
|
||
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 loadSeriesBindingShopCascadeOptions = async () => {
|
||
try {
|
||
const res = await ShopService.getShopsCascade({
|
||
parent_id: undefined,
|
||
exclude_self: true
|
||
})
|
||
if (res.code === 0) {
|
||
seriesBindingShopCascadeOptions.value = (res.data || []).map((item: any) => ({
|
||
value: item.id,
|
||
label: item.shop_name,
|
||
leaf: !item.has_children
|
||
}))
|
||
}
|
||
} catch (error) {
|
||
console.error('加载批量设置套餐系列根级店铺失败:', error)
|
||
}
|
||
}
|
||
|
||
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
|
||
const request: BatchSetDeviceSeriesBindingRequest = {
|
||
selection_type: seriesBindingForm.selection_type,
|
||
series_id: seriesBindingForm.series_id!
|
||
}
|
||
|
||
if (seriesBindingForm.selection_type === DeviceSelectionType.LIST) {
|
||
const deviceIds = selectedDevices.value.map((device) => device.id)
|
||
if (deviceIds.length === 0) {
|
||
ElMessage.warning('列表模式下请先选择要设置的设备')
|
||
return null
|
||
}
|
||
request.device_ids = deviceIds
|
||
return request
|
||
}
|
||
|
||
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE) {
|
||
request.virtual_no_start = seriesBindingForm.virtual_no_start
|
||
request.virtual_no_end = seriesBindingForm.virtual_no_end
|
||
return request
|
||
}
|
||
|
||
if (seriesBindingForm.virtual_no) request.virtual_no = seriesBindingForm.virtual_no
|
||
if (seriesBindingForm.device_name) request.device_name = seriesBindingForm.device_name
|
||
if (seriesBindingForm.device_type) request.device_type = seriesBindingForm.device_type
|
||
if (seriesBindingForm.manufacturer) request.manufacturer = seriesBindingForm.manufacturer
|
||
if (seriesBindingForm.batch_no) request.batch_no = seriesBindingForm.batch_no
|
||
if (seriesBindingForm.status !== undefined) request.status = seriesBindingForm.status
|
||
if (seriesBindingForm.shop_id_path?.length) {
|
||
request.shop_id = seriesBindingForm.shop_id_path[seriesBindingForm.shop_id_path.length - 1]
|
||
}
|
||
if (seriesBindingForm.filter_series_id !== undefined) {
|
||
request.filter_series_id = seriesBindingForm.filter_series_id
|
||
}
|
||
if (seriesBindingForm.created_at_range.length === 2) {
|
||
request.created_at_start = seriesBindingForm.created_at_range[0]
|
||
request.created_at_end = seriesBindingForm.created_at_range[1]
|
||
}
|
||
|
||
return request
|
||
}
|
||
|
||
// 确认设置套餐系列绑定
|
||
const handleConfirmSeriesBinding = async () => {
|
||
if (!seriesBindingFormRef.value) return
|
||
|
||
await seriesBindingFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
seriesBindingLoading.value = true
|
||
try {
|
||
const data = buildSeriesBindingRequest()
|
||
if (!data) {
|
||
return
|
||
}
|
||
const res = await DeviceService.batchSetDeviceSeriesBinding(data)
|
||
if (res.code === 0) {
|
||
seriesBindingResult.value = res.data
|
||
if (res.data.fail_count === 0) {
|
||
ElMessage.success('全部设置成功')
|
||
setTimeout(() => {
|
||
handleCloseSeriesBindingDialog()
|
||
getTableData()
|
||
}, 1500)
|
||
} else {
|
||
ElMessage.warning(`部分设置失败,请查看失败详情`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
seriesBindingLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 关闭套餐系列绑定对话框
|
||
const handleCloseSeriesBindingDialog = () => {
|
||
seriesBindingDialogVisible.value = false
|
||
seriesBindingResult.value = null
|
||
Object.assign(seriesBindingForm, createInitialSeriesBindingState())
|
||
if (seriesBindingFormRef.value) {
|
||
seriesBindingFormRef.value.resetFields()
|
||
}
|
||
}
|
||
|
||
// ========== 设备操作相关 ==========
|
||
|
||
// 获取设备IMEI(通过virtual_no)
|
||
const getDeviceImei = async (virtualNo: string): Promise<string | null> => {
|
||
try {
|
||
const res = await AssetService.resolveAsset(virtualNo)
|
||
if (res.code === 0 && res.data && res.data.asset_type === 'device') {
|
||
return res.data.imei || null
|
||
}
|
||
return null
|
||
} catch (error) {
|
||
console.error('获取设备IMEI失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 设备操作路由
|
||
const handleDeviceOperation = async (command: string, deviceNo: string, device?: Device) => {
|
||
// 设置当前操作的设备
|
||
currentOperatingDevice.value = device || null
|
||
|
||
switch (command) {
|
||
case 'view-cards':
|
||
handleViewCardsByDeviceNo(deviceNo)
|
||
break
|
||
case 'reboot': {
|
||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||
if (!imei) {
|
||
ElMessage.error('无法获取设备IMEI,无法执行重启操作')
|
||
return
|
||
}
|
||
handleRebootDevice(imei)
|
||
break
|
||
}
|
||
case 'reset': {
|
||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||
if (!imei) {
|
||
ElMessage.error('无法获取设备IMEI,无法执行重置操作')
|
||
return
|
||
}
|
||
handleResetDevice(imei)
|
||
break
|
||
}
|
||
case 'switch-card': {
|
||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||
if (!imei) {
|
||
ElMessage.error('无法获取设备IMEI,无法执行切卡操作')
|
||
return
|
||
}
|
||
await showSwitchCardDialog(imei)
|
||
break
|
||
}
|
||
case 'manual-deactivate':
|
||
handleManualDeactivateDevice()
|
||
break
|
||
case 'delete':
|
||
await handleDeleteDeviceByNo(deviceNo)
|
||
break
|
||
case 'clear-series':
|
||
await handleClearDeviceSeries(deviceNo)
|
||
break
|
||
case 'switch-mode':
|
||
await showSwitchModeDialog(deviceNo)
|
||
break
|
||
case 'realname-policy':
|
||
showRealnamePolicyDialog(deviceNo, device?.realname_policy)
|
||
break
|
||
}
|
||
}
|
||
|
||
// 显示实名认证策略对话框
|
||
const showRealnamePolicyDialog = (deviceNo: string, currentPolicy?: string) => {
|
||
currentRealnamePolicyAsset.value = deviceNo
|
||
currentRealnamePolicy.value = currentPolicy ?? 'none'
|
||
realnamePolicyDialogVisible.value = true
|
||
}
|
||
|
||
// 确认设置实名认证策略
|
||
const handleConfirmRealnamePolicy = async (data: { realname_policy: string }) => {
|
||
try {
|
||
const res = await AssetService.updateRealnamePolicy(
|
||
currentRealnamePolicyAsset.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 handleViewCardsByDeviceNo = (deviceNo: string) => {
|
||
const device = deviceList.value.find((d) => d.virtual_no === deviceNo)
|
||
if (device) {
|
||
handleViewCards(device)
|
||
} else {
|
||
ElMessage.error('未找到该设备')
|
||
}
|
||
}
|
||
|
||
// 手动停用设备(资产层面的停用)
|
||
const handleManualDeactivateDevice = () => {
|
||
const device = currentOperatingDevice.value
|
||
if (!device) {
|
||
ElMessage.error('未找到设备信息')
|
||
return
|
||
}
|
||
|
||
ElMessageBox.confirm(
|
||
`确定要手动停用设备 ${device.virtual_no} 吗?此操作将在资产管理层面停用该设备。`,
|
||
'确认手动停用',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
)
|
||
.then(async () => {
|
||
try {
|
||
const res = await AssetService.deactivateAsset(device.virtual_no)
|
||
if (res.code === 0) {
|
||
ElMessage.success('手动停用成功')
|
||
await getTableData()
|
||
}
|
||
} catch (error: any) {
|
||
console.error('手动停用失败:', error)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 通过设备号删除设备
|
||
const handleDeleteDeviceByNo = async (deviceNo: string) => {
|
||
// 先根据设备号找到设备对象
|
||
const device = deviceList.value.find((d) => d.virtual_no === deviceNo)
|
||
if (device) {
|
||
deleteDevice(device)
|
||
} else {
|
||
ElMessage.error('未找到该设备')
|
||
}
|
||
}
|
||
|
||
// 重启设备
|
||
const handleRebootDevice = (imei: string) => {
|
||
ElMessageBox.confirm(`确定要重启设备 ${imei} 吗?`, '重启确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
const res = await DeviceService.rebootDevice(imei)
|
||
if (res.code === 0) {
|
||
ElMessage.success('重启指令已发送')
|
||
} else {
|
||
ElMessage.error(res.msg || '重启失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('重启设备失败:', error)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 恢复出厂设置
|
||
const handleResetDevice = (imei: string) => {
|
||
ElMessageBox.confirm(
|
||
`确定要恢复设备 ${imei} 的出厂设置吗?此操作将清除所有配置和数据!`,
|
||
'恢复出厂设置确认',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'error'
|
||
}
|
||
)
|
||
.then(async () => {
|
||
try {
|
||
const res = await DeviceService.resetDevice(imei)
|
||
if (res.code === 0) {
|
||
ElMessage.success('恢复出厂设置指令已发送')
|
||
} else {
|
||
ElMessage.error(res.msg || '操作失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('恢复出厂设置失败:', error)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 显示切换SIM卡对话框
|
||
const showSwitchCardDialog = async (imei: string) => {
|
||
currentOperatingImei.value = imei
|
||
deviceBindingCards.value = []
|
||
switchCardDialogVisible.value = true
|
||
|
||
// 使用当前操作的设备
|
||
const device = currentOperatingDevice.value
|
||
if (!device) {
|
||
ElMessage.error('未找到设备信息')
|
||
return
|
||
}
|
||
|
||
// 加载设备绑定的卡列表
|
||
loadingDeviceCards.value = true
|
||
try {
|
||
const res = await DeviceService.getDeviceCards(device.virtual_no)
|
||
if (res.code === 0 && res.data) {
|
||
deviceBindingCards.value = res.data.bindings || []
|
||
if (deviceBindingCards.value.length === 0) {
|
||
ElMessage.warning('该设备暂无绑定的SIM卡')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载设备绑定卡列表失败:', error)
|
||
} finally {
|
||
loadingDeviceCards.value = false
|
||
}
|
||
}
|
||
|
||
// 确认切换SIM卡
|
||
const handleConfirmSwitchCard = async (data: { target_iccid: string }) => {
|
||
try {
|
||
const res = await DeviceService.switchCard(currentOperatingImei.value, {
|
||
target_iccid: data.target_iccid
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage.success('切换SIM卡指令已发送')
|
||
switchCardDialogVisible.value = false
|
||
} else {
|
||
ElMessage.error(res.msg || '切换失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('切换SIM卡失败:', error)
|
||
}
|
||
}
|
||
|
||
// 显示切换模式对话框
|
||
const normalizeSwitchModeCards = (cards: Array<Record<string, any>>): SwitchModeCardOption[] => {
|
||
return cards
|
||
.map((card) => ({
|
||
iot_card_id: Number(card.iot_card_id),
|
||
iccid: card.iccid,
|
||
slot_position: card.slot_position,
|
||
carrier_name: card.carrier_name,
|
||
msisdn: card.msisdn,
|
||
network_status: card.network_status === undefined ? undefined : Number(card.network_status),
|
||
real_name_status:
|
||
card.real_name_status === undefined ? undefined : Number(card.real_name_status),
|
||
is_current: card.is_current
|
||
}))
|
||
.filter((card) => Number.isFinite(card.iot_card_id) && Boolean(card.iccid))
|
||
}
|
||
|
||
// 显示切换模式对话框
|
||
const showSwitchModeDialog = async (deviceIdentifier: string) => {
|
||
const device = currentOperatingDevice.value
|
||
if (!device) {
|
||
ElMessage.error('未找到设备信息')
|
||
return
|
||
}
|
||
|
||
currentOperatingDeviceIdentifier.value = deviceIdentifier
|
||
currentDeviceSwitchMode.value = Number(device.switch_mode ?? 0)
|
||
currentOperatingDeviceHasImei.value = Boolean(device.imei)
|
||
switchModeCards.value = []
|
||
|
||
if (currentOperatingDeviceHasImei.value) {
|
||
try {
|
||
const res = await DeviceService.getDeviceCards(device.virtual_no)
|
||
if (res.code !== 0 || !res.data) {
|
||
ElMessage.error(res.msg || '加载设备绑定卡失败')
|
||
return
|
||
}
|
||
|
||
switchModeCards.value = normalizeSwitchModeCards(
|
||
(res.data.bindings || []) as Array<Record<string, any>>
|
||
)
|
||
} catch (error) {
|
||
console.error('加载切卡模式绑定卡失败:', error)
|
||
ElMessage.error('加载设备绑定卡失败')
|
||
return
|
||
}
|
||
}
|
||
|
||
switchModeDialogVisible.value = true
|
||
}
|
||
|
||
// 确认切换模式
|
||
const handleConfirmSwitchMode = async (data: { iot_card_id: number; switch_mode: 0 | 1 }) => {
|
||
switchModeLoading.value = true
|
||
try {
|
||
const res = await DeviceService.setSwitchMode(currentOperatingDeviceIdentifier.value, data)
|
||
if (res.code === 0) {
|
||
ElMessage.success('设置切卡模式成功')
|
||
switchModeDialogVisible.value = false
|
||
await getTableData()
|
||
} else {
|
||
ElMessage.error(res.msg || '设置切卡模式失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('设置切卡模式失败:', error)
|
||
} finally {
|
||
switchModeLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 设备操作菜单项配置
|
||
// 表格操作列配置
|
||
const getActions = (row: Device) => {
|
||
const actions: any[] = []
|
||
|
||
// 第一个按钮:切换SIM卡
|
||
if (hasAuth('device:switch_sim')) {
|
||
actions.push({
|
||
label: '切换SIM卡',
|
||
handler: () => handleDeviceOperation('switch-card', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
// 其他操作放到"更多"下拉菜单中
|
||
const moreActions: any[] = []
|
||
|
||
if (hasAuth('device:view_cards')) {
|
||
moreActions.push({
|
||
label: '设备绑定的卡',
|
||
handler: () => handleDeviceOperation('view-cards', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:switch_mode')) {
|
||
moreActions.push({
|
||
label: '设置切卡模式',
|
||
handler: () => handleDeviceOperation('switch-mode', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:reboot')) {
|
||
moreActions.push({
|
||
label: '重启设备',
|
||
handler: () => handleDeviceOperation('reboot', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:factory_reset')) {
|
||
moreActions.push({
|
||
label: '恢复出厂',
|
||
handler: () => handleDeviceOperation('reset', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:clear_series')) {
|
||
moreActions.push({
|
||
label: '清除关联',
|
||
handler: () => handleDeviceOperation('clear-series', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:realname_policy')) {
|
||
moreActions.push({
|
||
label: '实名认证策略',
|
||
handler: () => handleDeviceOperation('realname-policy', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
// if (hasAuth('device:manual_deactivate')) {
|
||
// moreActions.push({
|
||
// label: '手动停用',
|
||
// handler: () => handleDeviceOperation('manual-deactivate', row.virtual_no, row),
|
||
// type: 'primary'
|
||
// })
|
||
// }
|
||
|
||
if (hasAuth('device:delete')) {
|
||
moreActions.push({
|
||
label: '删除设备',
|
||
handler: () => handleDeviceOperation('delete', row.virtual_no, row),
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:operation_logs')) {
|
||
moreActions.push({
|
||
label: '操作审计日志',
|
||
handler: () => {
|
||
operationLogsIdentifier.value = row.virtual_no
|
||
operationLogsDialogVisible.value = true
|
||
},
|
||
type: 'primary'
|
||
})
|
||
}
|
||
|
||
// 如果有更多操作,添加到 actions 中
|
||
if (moreActions.length > 0) {
|
||
actions.push(...moreActions)
|
||
}
|
||
|
||
return actions
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.device-list-page {
|
||
height: 100%;
|
||
}
|
||
|
||
.range-allocation-hint {
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.range-allocation-hint--device {
|
||
margin: -6px 0 18px 120px;
|
||
}
|
||
|
||
:deep(.el-table__row.table-row-with-context-menu) {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.expiry-estimate--expiring {
|
||
font-weight: 500;
|
||
color: var(--el-color-danger);
|
||
}
|
||
</style>
|