1697 lines
53 KiB
Vue
1697 lines
53 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"
|
||
@click="handleBatchSetSeries"
|
||
:disabled="!selectedDevices.length"
|
||
v-permission="'device:batch_set_series'"
|
||
>
|
||
批量设置套餐系列
|
||
</ElButton>
|
||
</template>
|
||
</ArtTableHeader>
|
||
|
||
<!-- 表格 -->
|
||
<ArtTable
|
||
ref="tableRef"
|
||
row-key="id"
|
||
:loading="loading"
|
||
:data="deviceList"
|
||
:currentPage="pagination.page"
|
||
:pageSize="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:marginTop="10"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
@selection-change="handleSelectionChange"
|
||
@row-contextmenu="handleRowContextMenu"
|
||
>
|
||
<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">
|
||
<ElTreeSelect
|
||
v-model="allocateForm.target_shop_id"
|
||
:data="shopTreeData"
|
||
:props="{ label: 'shop_name', value: 'id', children: 'children' }"
|
||
placeholder="请选择目标店铺"
|
||
clearable
|
||
filterable
|
||
check-strictly
|
||
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.device_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.device_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="600px"
|
||
>
|
||
<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="series_id">
|
||
<ElSelect
|
||
v-model="seriesBindingForm.series_id"
|
||
placeholder="请选择或搜索套餐系列"
|
||
style="width: 100%"
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="searchPackageSeries"
|
||
:loading="seriesLoading"
|
||
clearable
|
||
>
|
||
<ElOption label="清除关联" :value="0" />
|
||
<ElOption
|
||
v-for="series in packageSeriesList"
|
||
:key="series.id"
|
||
:label="series.series_name"
|
||
:value="series.id"
|
||
:disabled="series.status !== 1"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
|
||
<!-- 设置结果 -->
|
||
<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.device_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="text-align: center; padding: 40px 0">
|
||
<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.device_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="900px">
|
||
<div style="margin-bottom: 10px; text-align: right">
|
||
<ElButton type="primary" @click="handleBindCard">绑定新卡</ElButton>
|
||
</div>
|
||
<div v-if="deviceCardsLoading" style="text-align: center; padding: 40px 0">
|
||
<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="100" align="center" />
|
||
<ElTableColumn prop="iccid" label="ICCID" min-width="180" />
|
||
<ElTableColumn prop="msisdn" label="接入号" width="140" />
|
||
<ElTableColumn prop="status" label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<ElTag :type="getCardStatusTagType(row.status)">
|
||
{{ getCardStatusText(row.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 type="danger" size="small" link @click="handleUnbindCard(row)">
|
||
解绑
|
||
</ElButton>
|
||
</template>
|
||
</ElTableColumn>
|
||
</ElTable>
|
||
<div
|
||
v-if="deviceCards.length === 0"
|
||
style="text-align: center; padding: 20px; color: #909399"
|
||
>
|
||
暂无绑定的卡片
|
||
</div>
|
||
</div>
|
||
</ElDialog>
|
||
|
||
<!-- 设备操作右键菜单 -->
|
||
<ArtMenuRight
|
||
ref="deviceOperationMenuRef"
|
||
:menu-items="deviceOperationMenuItems"
|
||
:menu-width="140"
|
||
@select="handleDeviceOperationMenuSelect"
|
||
/>
|
||
|
||
<!-- 绑定卡弹窗 -->
|
||
<ElDialog v-model="bindCardDialogVisible" title="绑定卡到设备" width="500px">
|
||
<ElForm
|
||
ref="bindCardFormRef"
|
||
:model="bindCardForm"
|
||
:rules="bindCardRules"
|
||
label-width="100px"
|
||
>
|
||
<ElFormItem label="IoT卡" prop="iot_card_id">
|
||
<ElSelect
|
||
v-model="bindCardForm.iot_card_id"
|
||
placeholder="请选择或搜索IoT卡(支持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.msisdn ? '(' + card.msisdn + ')' : ''}`"
|
||
:value="card.id"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<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>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="bindCardDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleConfirmBindCard" :loading="bindCardLoading">
|
||
确认绑定
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 设置限速对话框 -->
|
||
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="500px">
|
||
<ElForm
|
||
ref="speedLimitFormRef"
|
||
:model="speedLimitForm"
|
||
:rules="speedLimitRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="设备号">
|
||
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="下行速率" prop="download_speed">
|
||
<ElInputNumber
|
||
v-model="speedLimitForm.download_speed"
|
||
:min="1"
|
||
:step="128"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
|
||
</ElFormItem>
|
||
<ElFormItem label="上行速率" prop="upload_speed">
|
||
<ElInputNumber
|
||
v-model="speedLimitForm.upload_speed"
|
||
:min="1"
|
||
:step="128"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="speedLimitDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleConfirmSpeedLimit" :loading="speedLimitLoading">
|
||
确认设置
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 切换SIM卡对话框 -->
|
||
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="500px">
|
||
<ElForm
|
||
ref="switchCardFormRef"
|
||
:model="switchCardForm"
|
||
:rules="switchCardRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="设备号">
|
||
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="目标ICCID" prop="target_iccid">
|
||
<ElInput
|
||
v-model="switchCardForm.target_iccid"
|
||
placeholder="请输入要切换到的目标ICCID"
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="switchCardDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleConfirmSwitchCard" :loading="switchCardLoading">
|
||
确认切换
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 设置WiFi对话框 -->
|
||
<ElDialog v-model="setWiFiDialogVisible" title="设置WiFi" width="500px">
|
||
<ElForm
|
||
ref="setWiFiFormRef"
|
||
:model="setWiFiForm"
|
||
:rules="setWiFiRules"
|
||
label-width="120px"
|
||
>
|
||
<ElFormItem label="设备号">
|
||
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
|
||
</ElFormItem>
|
||
<ElFormItem label="WiFi状态" prop="enabled">
|
||
<ElRadioGroup v-model="setWiFiForm.enabled">
|
||
<ElRadio :value="1">启用</ElRadio>
|
||
<ElRadio :value="0">禁用</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
<ElFormItem label="WiFi名称" prop="ssid">
|
||
<ElInput
|
||
v-model="setWiFiForm.ssid"
|
||
placeholder="请输入WiFi名称(1-32个字符)"
|
||
maxlength="32"
|
||
show-word-limit
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem label="WiFi密码" prop="password">
|
||
<ElInput
|
||
v-model="setWiFiForm.password"
|
||
type="password"
|
||
placeholder="请输入WiFi密码(8-63个字符)"
|
||
maxlength="63"
|
||
show-word-limit
|
||
show-password
|
||
clearable
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="setWiFiDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleConfirmSetWiFi" :loading="setWiFiLoading">
|
||
确认设置
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</ElCard>
|
||
</div>
|
||
</ArtTableFullScreen>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { h } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { DeviceService, ShopService, CardService, PackageSeriesService } from '@/api/modules'
|
||
import {
|
||
ElMessage,
|
||
ElMessageBox,
|
||
ElTag,
|
||
ElSwitch,
|
||
ElIcon,
|
||
ElTreeSelect,
|
||
ElInputNumber,
|
||
ElRadioGroup,
|
||
ElRadio
|
||
} from 'element-plus'
|
||
import { Loading } from '@element-plus/icons-vue'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import type {
|
||
Device,
|
||
DeviceStatus,
|
||
AllocateDevicesResponse,
|
||
RecallDevicesResponse,
|
||
BatchSetDeviceSeriesBindingResponse
|
||
} from '@/types/api'
|
||
import type { SearchFormItem } from '@/types'
|
||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||
import { useAuth } from '@/composables/useAuth'
|
||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||
import { formatDateTime } from '@/utils/business/format'
|
||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||
import type { PackageSeriesResponse } from '@/types/api'
|
||
|
||
defineOptions({ name: 'DeviceList' })
|
||
|
||
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 shopTreeData = ref<any[]>([])
|
||
const allocateResult = ref<AllocateDevicesResponse | null>(null)
|
||
const recallResult = ref<RecallDevicesResponse | null>(null)
|
||
|
||
// 套餐系列绑定相关
|
||
const seriesBindingDialogVisible = ref(false)
|
||
const seriesBindingLoading = ref(false)
|
||
const seriesBindingFormRef = ref<FormInstance>()
|
||
const seriesLoading = ref(false)
|
||
const packageSeriesList = ref<PackageSeriesResponse[]>([])
|
||
const seriesBindingForm = reactive({
|
||
series_id: undefined as number | undefined
|
||
})
|
||
const seriesBindingRules = reactive<FormRules>({
|
||
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }]
|
||
})
|
||
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
||
|
||
// 设备详情弹窗相关
|
||
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 bindCardDialogVisible = ref(false)
|
||
const bindCardLoading = ref(false)
|
||
const bindCardFormRef = ref<FormInstance>()
|
||
const iotCardList = ref<any[]>([])
|
||
const iotCardSearchLoading = ref(false)
|
||
const bindCardForm = reactive({
|
||
iot_card_id: undefined as number | undefined,
|
||
slot_position: 1
|
||
})
|
||
const bindCardRules = reactive<FormRules>({
|
||
iot_card_id: [{ required: true, message: '请选择IoT卡', trigger: 'change' }],
|
||
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
|
||
})
|
||
|
||
// 设备操作相关对话框
|
||
const speedLimitDialogVisible = ref(false)
|
||
const speedLimitLoading = ref(false)
|
||
const speedLimitFormRef = ref<FormInstance>()
|
||
const speedLimitForm = reactive({
|
||
download_speed: 1024,
|
||
upload_speed: 512
|
||
})
|
||
const speedLimitRules = reactive<FormRules>({
|
||
download_speed: [
|
||
{ required: true, message: '请输入下行速率', trigger: 'blur' },
|
||
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
|
||
],
|
||
upload_speed: [
|
||
{ required: true, message: '请输入上行速率', trigger: 'blur' },
|
||
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
|
||
]
|
||
})
|
||
const currentOperatingDevice = ref<string>('')
|
||
|
||
// 设备操作右键菜单
|
||
const deviceOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||
const currentOperatingDeviceNo = ref<string>('')
|
||
|
||
const switchCardDialogVisible = ref(false)
|
||
const switchCardLoading = ref(false)
|
||
const switchCardFormRef = ref<FormInstance>()
|
||
const switchCardForm = reactive({
|
||
target_iccid: ''
|
||
})
|
||
const switchCardRules = reactive<FormRules>({
|
||
target_iccid: [{ required: true, message: '请输入目标ICCID', trigger: 'blur' }]
|
||
})
|
||
|
||
const setWiFiDialogVisible = ref(false)
|
||
const setWiFiLoading = ref(false)
|
||
const setWiFiFormRef = ref<FormInstance>()
|
||
const setWiFiForm = reactive({
|
||
enabled: 1,
|
||
ssid: '',
|
||
password: ''
|
||
})
|
||
const setWiFiRules = reactive<FormRules>({
|
||
ssid: [
|
||
{ required: true, message: '请输入WiFi名称', trigger: 'blur' },
|
||
{ min: 1, max: 32, message: 'WiFi名称长度为1-32个字符', trigger: 'blur' }
|
||
],
|
||
password: [
|
||
{ required: true, message: '请输入WiFi密码', trigger: 'blur' },
|
||
{ min: 8, max: 63, message: 'WiFi密码长度为8-63个字符', trigger: 'blur' }
|
||
]
|
||
})
|
||
|
||
// 搜索表单初始值
|
||
const initialSearchState = {
|
||
device_no: '',
|
||
device_name: '',
|
||
status: undefined as DeviceStatus | undefined,
|
||
batch_no: '',
|
||
device_type: '',
|
||
manufacturer: ''
|
||
}
|
||
|
||
// 搜索表单
|
||
const searchForm = reactive({ ...initialSearchState })
|
||
|
||
// 搜索表单配置
|
||
const searchFormItems: SearchFormItem[] = [
|
||
{
|
||
label: '设备号',
|
||
prop: 'device_no',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入设备号'
|
||
}
|
||
},
|
||
{
|
||
label: '设备名称',
|
||
prop: 'device_name',
|
||
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: 'batch_no',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入批次号'
|
||
}
|
||
},
|
||
{
|
||
label: '设备类型',
|
||
prop: 'device_type',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入设备类型'
|
||
}
|
||
},
|
||
{
|
||
label: '制造商',
|
||
prop: 'manufacturer',
|
||
type: 'input',
|
||
config: {
|
||
clearable: true,
|
||
placeholder: '请输入制造商'
|
||
}
|
||
}
|
||
]
|
||
|
||
// 分页
|
||
const pagination = reactive({
|
||
page: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 列配置
|
||
const columnOptions = [
|
||
{ label: '设备号', prop: 'device_no' },
|
||
{ label: '设备名称', prop: 'device_name' },
|
||
{ label: '设备型号', prop: 'device_model' },
|
||
{ label: '设备类型', prop: 'device_type' },
|
||
{ label: '制造商', prop: 'manufacturer' },
|
||
{ label: '最大插槽数', prop: 'max_sim_slots' },
|
||
{ label: '已绑定卡数', prop: 'bound_card_count' },
|
||
{ label: '状态', prop: 'status' },
|
||
{ label: '批次号', prop: 'batch_no' },
|
||
{ 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) => {
|
||
router.push({
|
||
path: '/asset-management/device-detail',
|
||
query: {
|
||
device_no: deviceNo
|
||
}
|
||
})
|
||
}
|
||
|
||
// 查看设备绑定的卡片
|
||
const handleViewCards = async (device: Device) => {
|
||
currentDeviceDetail.value = device
|
||
deviceCards.value = []
|
||
deviceCardsDialogVisible.value = true
|
||
await loadDeviceCards(device.id)
|
||
}
|
||
|
||
// 加载设备绑定的卡列表
|
||
const loadDeviceCards = async (deviceId: number) => {
|
||
deviceCardsLoading.value = true
|
||
try {
|
||
const res = await DeviceService.getDeviceCards(deviceId)
|
||
if (res.code === 0 && res.data) {
|
||
deviceCards.value = res.data.bindings || []
|
||
}
|
||
} catch (error) {
|
||
console.error('获取设备绑定的卡列表失败:', error)
|
||
} finally {
|
||
deviceCardsLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 打开绑定卡弹窗
|
||
const handleBindCard = async () => {
|
||
bindCardForm.iot_card_id = undefined
|
||
bindCardForm.slot_position = 1
|
||
bindCardDialogVisible.value = true
|
||
// 加载默认的IoT卡列表
|
||
await loadDefaultIotCards()
|
||
}
|
||
|
||
// 加载默认的IoT卡列表
|
||
const loadDefaultIotCards = async () => {
|
||
iotCardSearchLoading.value = true
|
||
try {
|
||
const res = await CardService.getStandaloneIotCards({
|
||
page: 1,
|
||
page_size: 20
|
||
})
|
||
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
|
||
try {
|
||
const res = await CardService.getStandaloneIotCards({
|
||
page: 1,
|
||
page_size: 20,
|
||
iccid: query
|
||
})
|
||
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.id, {
|
||
iot_card_id: bindCardForm.iot_card_id!,
|
||
slot_position: bindCardForm.slot_position
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage.success('绑定成功')
|
||
bindCardDialogVisible.value = false
|
||
// 重新加载卡列表
|
||
await loadDeviceCards(currentDeviceDetail.value.id)
|
||
// 刷新设备详情以更新绑定卡数量
|
||
const detailRes = await DeviceService.getDeviceByImei(
|
||
currentDeviceDetail.value.device_no
|
||
)
|
||
if (detailRes.code === 0 && detailRes.data) {
|
||
currentDeviceDetail.value = detailRes.data
|
||
}
|
||
} else {
|
||
ElMessage.error(res.message || '绑定失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('绑定卡失败:', error)
|
||
ElMessage.error(error?.message || '绑定失败')
|
||
} 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.id, row.iot_card_id)
|
||
if (res.code === 0) {
|
||
ElMessage.success('解绑成功')
|
||
// 重新加载卡列表
|
||
await loadDeviceCards(currentDeviceDetail.value.id)
|
||
// 刷新设备详情以更新绑定卡数量
|
||
const detailRes = await DeviceService.getDeviceByImei(
|
||
currentDeviceDetail.value.device_no
|
||
)
|
||
if (detailRes.code === 0 && detailRes.data) {
|
||
currentDeviceDetail.value = detailRes.data
|
||
}
|
||
} else {
|
||
ElMessage.error(res.message || '解绑失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('解绑卡失败:', error)
|
||
ElMessage.error(error?.message || '解绑失败')
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 获取卡状态标签类型
|
||
const getCardStatusTagType = (status: number) => {
|
||
const typeMap: Record<number, any> = {
|
||
1: 'info', // 在库
|
||
2: 'warning', // 已分销
|
||
3: 'success', // 已激活
|
||
4: 'danger' // 已停用
|
||
}
|
||
return typeMap[status] || 'info'
|
||
}
|
||
|
||
// 获取卡状态文本
|
||
const getCardStatusText = (status: number) => {
|
||
const textMap: Record<number, string> = {
|
||
1: '在库',
|
||
2: '已分销',
|
||
3: '已激活',
|
||
4: '已停用'
|
||
}
|
||
return textMap[status] || '未知'
|
||
}
|
||
|
||
// 获取设备状态标签类型
|
||
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: 'device_no',
|
||
label: '设备号',
|
||
minWidth: 150,
|
||
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.device_no)
|
||
}
|
||
},
|
||
row.device_no
|
||
)
|
||
}
|
||
},
|
||
{
|
||
prop: 'device_name',
|
||
label: '设备名称',
|
||
minWidth: 120
|
||
},
|
||
{
|
||
prop: 'device_model',
|
||
label: '设备型号',
|
||
minWidth: 120
|
||
},
|
||
{
|
||
prop: 'device_type',
|
||
label: '设备类型',
|
||
width: 120
|
||
},
|
||
{
|
||
prop: 'manufacturer',
|
||
label: '制造商',
|
||
minWidth: 120
|
||
},
|
||
{
|
||
prop: 'max_sim_slots',
|
||
label: '最大插槽数',
|
||
width: 100,
|
||
align: 'center'
|
||
},
|
||
{
|
||
prop: 'bound_card_count',
|
||
label: '已绑定卡数',
|
||
width: 110,
|
||
align: 'center',
|
||
formatter: (row: Device) => {
|
||
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
|
||
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
|
||
}
|
||
},
|
||
{
|
||
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: 'batch_no',
|
||
label: '批次号',
|
||
minWidth: 180,
|
||
formatter: (row: Device) => row.batch_no || '-'
|
||
},
|
||
{
|
||
prop: 'created_at',
|
||
label: '创建时间',
|
||
width: 180,
|
||
formatter: (row: Device) => formatDateTime(row.created_at)
|
||
}
|
||
])
|
||
|
||
onMounted(() => {
|
||
getTableData()
|
||
loadShopTree()
|
||
})
|
||
|
||
// 当页面被 keep-alive 激活时自动刷新数据
|
||
onActivated(() => {
|
||
getTableData()
|
||
})
|
||
|
||
// 加载店铺树形数据
|
||
const loadShopTree = async () => {
|
||
try {
|
||
const res = await ShopService.getShops({
|
||
page: 1,
|
||
page_size: 9999, // 获取所有数据用于构建树形结构
|
||
status: 1 // 只获取启用的店铺
|
||
})
|
||
if (res.code === 0) {
|
||
shopTreeData.value = buildShopTree(res.data.items || [])
|
||
}
|
||
} catch (error) {
|
||
console.error('获取店铺列表失败:', error)
|
||
}
|
||
}
|
||
|
||
// 构建店铺树形结构
|
||
const buildShopTree = (shops: any[]) => {
|
||
const map = new Map<number, any>()
|
||
const tree: any[] = []
|
||
|
||
// 先将所有项放入 map
|
||
shops.forEach((shop) => {
|
||
map.set(shop.id, { ...shop, children: [] })
|
||
})
|
||
|
||
// 构建树形结构
|
||
shops.forEach((shop) => {
|
||
const node = map.get(shop.id)!
|
||
if (shop.parent_id && map.has(shop.parent_id)) {
|
||
// 有父节点,添加到父节点的 children 中
|
||
const parent = map.get(shop.parent_id)!
|
||
if (!parent.children) parent.children = []
|
||
parent.children.push(node)
|
||
} else {
|
||
// 没有父节点或父节点不存在,作为根节点
|
||
tree.push(node)
|
||
}
|
||
})
|
||
|
||
return tree
|
||
}
|
||
|
||
// 获取设备列表
|
||
const getTableData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
page: pagination.page,
|
||
page_size: pagination.pageSize,
|
||
device_no: searchForm.device_no || undefined,
|
||
device_name: searchForm.device_name || undefined,
|
||
status: searchForm.status,
|
||
batch_no: searchForm.batch_no || undefined,
|
||
device_type: searchForm.device_type || undefined,
|
||
manufacturer: searchForm.manufacturer || 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)
|
||
ElMessage.error('获取设备列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 重置搜索
|
||
const handleReset = () => {
|
||
Object.assign(searchForm, { ...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: Device[]) => {
|
||
selectedDevices.value = selection
|
||
}
|
||
|
||
// 删除设备
|
||
const deleteDevice = (row: Device) => {
|
||
ElMessageBox.confirm(`确定删除设备 ${row.device_no} 吗?删除后将自动解绑所有卡。`, '删除确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'error'
|
||
})
|
||
.then(async () => {
|
||
try {
|
||
await DeviceService.deleteDevice(row.id)
|
||
ElMessage.success('删除成功')
|
||
await getTableData()
|
||
} catch (error) {
|
||
console.error(error)
|
||
ElMessage.error('删除失败')
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消删除
|
||
})
|
||
}
|
||
|
||
// 批量分配
|
||
const handleBatchAllocate = async () => {
|
||
if (selectedDevices.value.length === 0) {
|
||
ElMessage.warning('请先选择要分配的设备')
|
||
return
|
||
}
|
||
allocateForm.target_shop_id = undefined
|
||
allocateForm.remark = ''
|
||
allocateResult.value = null
|
||
allocateDialogVisible.value = true
|
||
}
|
||
|
||
// 确认批量分配
|
||
const handleConfirmAllocate = async () => {
|
||
if (!allocateFormRef.value) return
|
||
|
||
await allocateFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
allocateLoading.value = true
|
||
try {
|
||
const data = {
|
||
device_ids: selectedDevices.value.map((d) => d.id),
|
||
target_shop_id: allocateForm.target_shop_id!,
|
||
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)
|
||
ElMessage.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)
|
||
ElMessage.error('回收失败')
|
||
} finally {
|
||
recallLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 关闭回收对话框
|
||
const handleCloseRecallDialog = () => {
|
||
recallDialogVisible.value = false
|
||
recallResult.value = null
|
||
recallForm.remark = ''
|
||
}
|
||
|
||
// 批量设置套餐系列
|
||
const handleBatchSetSeries = async () => {
|
||
if (selectedDevices.value.length === 0) {
|
||
ElMessage.warning('请先选择要设置的设备')
|
||
return
|
||
}
|
||
seriesBindingForm.series_id = undefined
|
||
seriesBindingResult.value = null
|
||
await loadPackageSeriesList()
|
||
seriesBindingDialogVisible.value = true
|
||
}
|
||
|
||
// 加载套餐系列列表(支持名称搜索,默认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)
|
||
ElMessage.error('获取套餐系列列表失败')
|
||
} finally {
|
||
seriesLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 搜索套餐系列
|
||
const searchPackageSeries = async (query: string) => {
|
||
await loadPackageSeriesList(query || undefined)
|
||
}
|
||
|
||
// 确认设置套餐系列绑定
|
||
const handleConfirmSeriesBinding = async () => {
|
||
if (!seriesBindingFormRef.value) return
|
||
|
||
await seriesBindingFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
seriesBindingLoading.value = true
|
||
try {
|
||
const data = {
|
||
device_ids: selectedDevices.value.map((d) => d.id),
|
||
series_id: seriesBindingForm.series_id!
|
||
}
|
||
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
|
||
if (seriesBindingFormRef.value) {
|
||
seriesBindingFormRef.value.resetFields()
|
||
}
|
||
}
|
||
|
||
// ========== 设备操作相关 ==========
|
||
|
||
// 设备操作路由
|
||
const handleDeviceOperation = (command: string, deviceNo: string) => {
|
||
switch (command) {
|
||
case 'view-cards':
|
||
handleViewCardsByDeviceNo(deviceNo)
|
||
break
|
||
case 'reboot':
|
||
handleRebootDevice(deviceNo)
|
||
break
|
||
case 'reset':
|
||
handleResetDevice(deviceNo)
|
||
break
|
||
case 'speed-limit':
|
||
showSpeedLimitDialog(deviceNo)
|
||
break
|
||
case 'switch-card':
|
||
showSwitchCardDialog(deviceNo)
|
||
break
|
||
case 'set-wifi':
|
||
showSetWiFiDialog(deviceNo)
|
||
break
|
||
case 'delete':
|
||
handleDeleteDeviceByNo(deviceNo)
|
||
break
|
||
}
|
||
}
|
||
|
||
// 通过设备号查看卡片
|
||
const handleViewCardsByDeviceNo = (deviceNo: string) => {
|
||
const device = deviceList.value.find((d) => d.device_no === deviceNo)
|
||
if (device) {
|
||
handleViewCards(device)
|
||
} else {
|
||
ElMessage.error('未找到该设备')
|
||
}
|
||
}
|
||
|
||
// 通过设备号删除设备
|
||
const handleDeleteDeviceByNo = async (deviceNo: string) => {
|
||
// 先根据设备号找到设备对象
|
||
const device = deviceList.value.find((d) => d.device_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.message || '重启失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('重启设备失败:', error)
|
||
ElMessage.error(error?.message || '重启失败')
|
||
}
|
||
})
|
||
.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.message || '操作失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('恢复出厂设置失败:', error)
|
||
ElMessage.error(error?.message || '操作失败')
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 用户取消
|
||
})
|
||
}
|
||
|
||
// 显示设置限速对话框
|
||
const showSpeedLimitDialog = (imei: string) => {
|
||
currentOperatingDevice.value = imei
|
||
speedLimitForm.download_speed = 1024
|
||
speedLimitForm.upload_speed = 512
|
||
speedLimitDialogVisible.value = true
|
||
}
|
||
|
||
// 确认设置限速
|
||
const handleConfirmSpeedLimit = async () => {
|
||
if (!speedLimitFormRef.value) return
|
||
|
||
await speedLimitFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
speedLimitLoading.value = true
|
||
try {
|
||
const res = await DeviceService.setSpeedLimit(currentOperatingDevice.value, {
|
||
download_speed: speedLimitForm.download_speed,
|
||
upload_speed: speedLimitForm.upload_speed
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage.success('限速设置成功')
|
||
speedLimitDialogVisible.value = false
|
||
} else {
|
||
ElMessage.error(res.message || '设置失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('设置限速失败:', error)
|
||
ElMessage.error(error?.message || '设置失败')
|
||
} finally {
|
||
speedLimitLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 显示切换SIM卡对话框
|
||
const showSwitchCardDialog = (imei: string) => {
|
||
currentOperatingDevice.value = imei
|
||
switchCardForm.target_iccid = ''
|
||
switchCardDialogVisible.value = true
|
||
}
|
||
|
||
// 确认切换SIM卡
|
||
const handleConfirmSwitchCard = async () => {
|
||
if (!switchCardFormRef.value) return
|
||
|
||
await switchCardFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
switchCardLoading.value = true
|
||
try {
|
||
const res = await DeviceService.switchCard(currentOperatingDevice.value, {
|
||
target_iccid: switchCardForm.target_iccid
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage.success('切换SIM卡指令已发送')
|
||
switchCardDialogVisible.value = false
|
||
} else {
|
||
ElMessage.error(res.message || '切换失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('切换SIM卡失败:', error)
|
||
ElMessage.error(error?.message || '切换失败')
|
||
} finally {
|
||
switchCardLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 显示设置WiFi对话框
|
||
const showSetWiFiDialog = (imei: string) => {
|
||
currentOperatingDevice.value = imei
|
||
setWiFiForm.enabled = 1
|
||
setWiFiForm.ssid = ''
|
||
setWiFiForm.password = ''
|
||
setWiFiDialogVisible.value = true
|
||
}
|
||
|
||
// 确认设置WiFi
|
||
const handleConfirmSetWiFi = async () => {
|
||
if (!setWiFiFormRef.value) return
|
||
|
||
await setWiFiFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
setWiFiLoading.value = true
|
||
try {
|
||
const res = await DeviceService.setWiFi(currentOperatingDevice.value, {
|
||
enabled: setWiFiForm.enabled,
|
||
ssid: setWiFiForm.ssid,
|
||
password: setWiFiForm.password
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage.success('WiFi设置成功')
|
||
setWiFiDialogVisible.value = false
|
||
} else {
|
||
ElMessage.error(res.message || '设置失败')
|
||
}
|
||
} catch (error: any) {
|
||
console.error('设置WiFi失败:', error)
|
||
ElMessage.error(error?.message || '设置失败')
|
||
} finally {
|
||
setWiFiLoading.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 设备操作菜单项配置
|
||
const deviceOperationMenuItems = computed((): MenuItemType[] => {
|
||
const items: MenuItemType[] = []
|
||
|
||
// 添加查看卡片到菜单最前面
|
||
if (hasAuth('device:view_cards')) {
|
||
items.push({
|
||
key: 'view-cards',
|
||
label: '查看卡片'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:reboot')) {
|
||
items.push({
|
||
key: 'reboot',
|
||
label: '重启设备'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:factory_reset')) {
|
||
items.push({
|
||
key: 'reset',
|
||
label: '恢复出厂'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:set_speed_limit')) {
|
||
items.push({
|
||
key: 'speed-limit',
|
||
label: '设置限速'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:switch_sim')) {
|
||
items.push({
|
||
key: 'switch-card',
|
||
label: '切换SIM卡'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:set_wifi')) {
|
||
items.push({
|
||
key: 'set-wifi',
|
||
label: '设置WiFi'
|
||
})
|
||
}
|
||
|
||
if (hasAuth('device:delete')) {
|
||
items.push({
|
||
key: 'delete',
|
||
label: '删除设备'
|
||
})
|
||
}
|
||
|
||
return items
|
||
})
|
||
|
||
// 显示设备操作菜单
|
||
const showDeviceOperationMenu = (e: MouseEvent, deviceNo: string) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
currentOperatingDeviceNo.value = deviceNo
|
||
deviceOperationMenuRef.value?.show(e)
|
||
}
|
||
|
||
// 处理设备操作菜单选择
|
||
const handleDeviceOperationMenuSelect = (item: MenuItemType) => {
|
||
const deviceNo = currentOperatingDeviceNo.value
|
||
if (!deviceNo) return
|
||
|
||
handleDeviceOperation(item.key, deviceNo)
|
||
}
|
||
|
||
// 处理表格行右键菜单
|
||
const handleRowContextMenu = (row: Device, column: any, event: MouseEvent) => {
|
||
showDeviceOperationMenu(event, row.device_no)
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.device-list-page {
|
||
height: 100%;
|
||
}
|
||
</style>
|