This commit is contained in:
@@ -54,6 +54,14 @@ export class DeviceService extends BaseService {
|
|||||||
return this.getOne<Device>(`/api/admin/devices/by-imei/${imei}`)
|
return this.getOne<Device>(`/api/admin/devices/by-imei/${imei}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过ICCID查询设备详情
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static getDeviceByIccid(iccid: string): Promise<BaseResponse<any>> {
|
||||||
|
return this.getOne<any>(`/api/admin/devices/by-iccid/${iccid}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除设备
|
* 删除设备
|
||||||
* @param id 设备ID
|
* @param id 设备ID
|
||||||
|
|||||||
@@ -444,6 +444,7 @@
|
|||||||
"deviceSearch": "设备查询",
|
"deviceSearch": "设备查询",
|
||||||
"singleCard": "单卡信息",
|
"singleCard": "单卡信息",
|
||||||
"standaloneCardList": "IoT卡管理",
|
"standaloneCardList": "IoT卡管理",
|
||||||
|
"iotCardDetail": "IoT卡详情",
|
||||||
"iotCardTask": "IoT卡任务",
|
"iotCardTask": "IoT卡任务",
|
||||||
"deviceTask": "设备任务",
|
"deviceTask": "设备任务",
|
||||||
"taskDetail": "任务详情",
|
"taskDetail": "任务详情",
|
||||||
|
|||||||
@@ -977,6 +977,16 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'iot-card-management/detail',
|
||||||
|
name: 'IotCardDetail',
|
||||||
|
component: RoutesAlias.StandaloneCardList + '/detail',
|
||||||
|
meta: {
|
||||||
|
title: 'menus.assetManagement.iotCardDetail',
|
||||||
|
isHide: true,
|
||||||
|
keepAlive: false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'iot-card-task',
|
path: 'iot-card-task',
|
||||||
name: 'IotCardTask',
|
name: 'IotCardTask',
|
||||||
|
|||||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -109,6 +109,7 @@ declare module 'vue' {
|
|||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
|
|||||||
@@ -1,94 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="device-detail-page">
|
<div class="device-detail">
|
||||||
<ElPageHeader @back="handleBack" title="设备详情" />
|
<ElCard shadow="never">
|
||||||
|
<!-- 页面头部 -->
|
||||||
<ElCard shadow="never" style="margin-top: 20px" v-loading="loading">
|
<div class="detail-header">
|
||||||
<template #header>
|
<ElButton @click="handleBack">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
<template #icon>
|
||||||
<span style="font-weight: bold">基本信息</span>
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
<ElTag :type="statusTypeMap[deviceInfo?.status || 1]">
|
|
||||||
{{ deviceInfo?.status_name || '-' }}
|
|
||||||
</ElTag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElDescriptions :column="3" border v-if="deviceInfo">
|
|
||||||
<ElDescriptionsItem label="设备ID">{{ deviceInfo.id }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="设备号">{{ deviceInfo.device_no }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="设备名称">{{ deviceInfo.device_name }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="设备型号">{{ deviceInfo.device_model }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="设备类型">{{ deviceInfo.device_type }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="制造商">{{ deviceInfo.manufacturer }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="最大插槽数">{{ deviceInfo.max_sim_slots }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="已绑定卡数">
|
|
||||||
<span style="font-weight: bold; color: #67c23a">{{ deviceInfo.bound_card_count }}</span>
|
|
||||||
/ {{ deviceInfo.max_sim_slots }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="所属店铺">
|
|
||||||
{{ deviceInfo.shop_name || '平台库存' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="批次号">
|
|
||||||
{{ deviceInfo.batch_no || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="激活时间">
|
|
||||||
{{ deviceInfo.activated_at ? formatDateTime(deviceInfo.activated_at) : '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="创建时间">
|
|
||||||
{{ formatDateTime(deviceInfo.created_at) }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElCard shadow="never" style="margin-top: 20px" v-loading="cardsLoading">
|
|
||||||
<template #header>
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
|
||||||
<span style="font-weight: bold">绑定的卡列表</span>
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="showBindDialog"
|
|
||||||
:disabled="!deviceInfo || deviceInfo.bound_card_count >= deviceInfo.max_sim_slots"
|
|
||||||
>
|
|
||||||
绑定新卡
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElTable :data="cardList" border>
|
|
||||||
<ElTableColumn prop="slot_position" label="插槽位置" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<ElTag type="info" size="small">插槽 {{ row.slot_position }}</ElTag>
|
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
返回
|
||||||
<ElTableColumn prop="iccid" label="ICCID" minWidth="180" />
|
</ElButton>
|
||||||
<ElTableColumn prop="msisdn" label="接入号" width="150">
|
<h2 class="detail-title">设备详情</h2>
|
||||||
<template #default="{ row }">
|
</div>
|
||||||
{{ row.msisdn || '-' }}
|
|
||||||
</template>
|
<!-- 详情内容 -->
|
||||||
</ElTableColumn>
|
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
|
||||||
<ElTableColumn prop="carrier_name" label="运营商" width="120" />
|
|
||||||
<ElTableColumn prop="status" label="卡状态" width="100">
|
<!-- 加载中 -->
|
||||||
<template #default="{ row }">
|
<div v-if="loading" class="loading-container">
|
||||||
<ElTag :type="cardStatusTypeMap[row.status]" size="small">
|
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||||
{{ cardStatusTextMap[row.status] || '未知' }}
|
<span>加载中...</span>
|
||||||
</ElTag>
|
</div>
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
<!-- 绑定卡片列表 -->
|
||||||
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
|
<ElCard v-if="detailData" shadow="never" class="cards-section" style="margin-top: 20px">
|
||||||
<template #default="{ row }">
|
<template #header>
|
||||||
{{ row.bind_time ? formatDateTime(row.bind_time) : '-' }}
|
<div class="section-header">
|
||||||
</template>
|
<span class="section-title">绑定的卡列表</span>
|
||||||
</ElTableColumn>
|
<ElButton
|
||||||
<ElTableColumn label="操作" width="100" fixed="right" align="center">
|
type="primary"
|
||||||
<template #default="{ row }">
|
size="small"
|
||||||
<ElButton type="danger" text size="small" @click="handleUnbindCard(row)">
|
@click="showBindDialog"
|
||||||
解绑
|
:disabled="!detailData || detailData.bound_card_count >= detailData.max_sim_slots"
|
||||||
|
>
|
||||||
|
绑定新卡
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</div>
|
||||||
</ElTableColumn>
|
</template>
|
||||||
</ElTable>
|
|
||||||
|
|
||||||
<ElEmpty v-if="!cardList.length" description="暂无绑定的卡" />
|
<ElTable :data="cardList" border v-loading="cardsLoading">
|
||||||
|
<ElTableColumn prop="slot_position" label="插槽位置" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag type="info" size="small">插槽 {{ row.slot_position }}</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="iccid" label="ICCID" minWidth="180" />
|
||||||
|
<ElTableColumn prop="msisdn" label="接入号" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.msisdn || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="carrier_name" label="运营商" width="120" />
|
||||||
|
<ElTableColumn prop="status" label="卡状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag :type="cardStatusTypeMap[row.status]" size="small">
|
||||||
|
{{ cardStatusTextMap[row.status] || '未知' }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="bind_time" label="绑定时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.bind_time ? formatDateTime(row.bind_time) : '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="操作" width="100" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElButton type="danger" text size="small" @click="handleUnbindCard(row)">
|
||||||
|
解绑
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
|
||||||
|
<ElEmpty v-if="!cardList.length && !cardsLoading" description="暂无绑定的卡" />
|
||||||
|
</ElCard>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<!-- 绑定卡对话框 -->
|
<!-- 绑定卡对话框 -->
|
||||||
@@ -145,24 +129,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, computed, h } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElCard, ElButton, ElIcon, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
import { DeviceService, CardService } from '@/api/modules'
|
import { DeviceService, CardService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import type { Device, DeviceCardBinding } from '@/types/api'
|
import type { Device, DeviceCardBinding } from '@/types/api'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceDetail' })
|
defineOptions({ name: 'DeviceDetail' })
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const cardsLoading = ref(false)
|
const cardsLoading = ref(false)
|
||||||
const bindLoading = ref(false)
|
const bindLoading = ref(false)
|
||||||
const searchCardsLoading = ref(false)
|
const searchCardsLoading = ref(false)
|
||||||
const bindDialogVisible = ref(false)
|
const bindDialogVisible = ref(false)
|
||||||
const bindFormRef = ref<FormInstance>()
|
const bindFormRef = ref<FormInstance>()
|
||||||
const deviceInfo = ref<Device | null>(null)
|
const detailData = ref<Device | null>(null)
|
||||||
const cardList = ref<DeviceCardBinding[]>([])
|
const cardList = ref<DeviceCardBinding[]>([])
|
||||||
const availableCards = ref<any[]>([])
|
const availableCards = ref<any[]>([])
|
||||||
|
|
||||||
@@ -204,34 +193,88 @@
|
|||||||
|
|
||||||
// 可用插槽
|
// 可用插槽
|
||||||
const availableSlots = computed(() => {
|
const availableSlots = computed(() => {
|
||||||
if (!deviceInfo.value) return []
|
if (!detailData.value) return []
|
||||||
const occupiedSlots = cardList.value.map((card) => card.slot_position)
|
const occupiedSlots = cardList.value.map((card) => card.slot_position)
|
||||||
const allSlots = Array.from({ length: deviceInfo.value.max_sim_slots }, (_, i) => i + 1)
|
const allSlots = Array.from({ length: detailData.value.max_sim_slots }, (_, i) => i + 1)
|
||||||
return allSlots.filter((slot) => !occupiedSlots.includes(slot))
|
return allSlots.filter((slot) => !occupiedSlots.includes(slot))
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
// 详情页配置
|
||||||
const deviceId = route.query.id
|
const detailSections: DetailSection[] = [
|
||||||
if (deviceId) {
|
{
|
||||||
loadDeviceInfo(Number(deviceId))
|
title: '基本信息',
|
||||||
loadDeviceCards(Number(deviceId))
|
fields: [
|
||||||
} else {
|
{ label: '设备ID', prop: 'id' },
|
||||||
ElMessage.error('设备ID不存在')
|
{ label: '设备号', prop: 'device_no' },
|
||||||
handleBack()
|
{ label: '设备名称', prop: 'device_name', formatter: (value) => value || '-' },
|
||||||
|
{ label: '设备型号', prop: 'device_model', formatter: (value) => value || '-' },
|
||||||
|
{ label: '设备类型', prop: 'device_type', formatter: (value) => value || '-' },
|
||||||
|
{ label: '制造商', prop: 'manufacturer', formatter: (value) => value || '-' },
|
||||||
|
{ label: '最大插槽数', prop: 'max_sim_slots' },
|
||||||
|
{
|
||||||
|
label: '已绑定卡数',
|
||||||
|
render: (data: Device) => {
|
||||||
|
const color = data.bound_card_count > 0 ? '#67c23a' : '#909399'
|
||||||
|
return h('span', { style: { color, fontWeight: 'bold' } },
|
||||||
|
`${data.bound_card_count} / ${data.max_sim_slots}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
render: (data: 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[data.status] || { text: '未知', type: 'info' }
|
||||||
|
return h(ElTag, { type: status.type }, () => status.text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: '所属店铺', prop: 'shop_name', formatter: (value) => value || '平台库存' },
|
||||||
|
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
|
||||||
|
{
|
||||||
|
label: '激活时间',
|
||||||
|
prop: 'activated_at',
|
||||||
|
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||||
|
},
|
||||||
|
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
]
|
||||||
|
|
||||||
// 加载设备信息
|
// 返回上一页
|
||||||
const loadDeviceInfo = async (id: number) => {
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取详情数据
|
||||||
|
const fetchDetail = async (id?: number, deviceNo?: string) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await DeviceService.getDeviceById(id)
|
let res
|
||||||
if (res.code === 0) {
|
if (id) {
|
||||||
deviceInfo.value = res.data
|
res = await DeviceService.getDeviceById(id)
|
||||||
|
} else if (deviceNo) {
|
||||||
|
res = await DeviceService.getDeviceByImei(deviceNo)
|
||||||
|
} else {
|
||||||
|
ElMessage.error('缺少设备参数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
detailData.value = res.data
|
||||||
|
// 加载绑定的卡列表
|
||||||
|
if (res.data.id) {
|
||||||
|
loadDeviceCards(res.data.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '获取设备详情失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
ElMessage.error('获取设备信息失败')
|
ElMessage.error('获取设备详情失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -267,7 +310,7 @@
|
|||||||
page_size: 20
|
page_size: 20
|
||||||
})
|
})
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
availableCards.value = res.data.list || []
|
availableCards.value = res.data.items || []
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -286,7 +329,7 @@
|
|||||||
|
|
||||||
// 确认绑定
|
// 确认绑定
|
||||||
const handleConfirmBind = async () => {
|
const handleConfirmBind = async () => {
|
||||||
if (!bindFormRef.value || !deviceInfo.value) return
|
if (!bindFormRef.value || !detailData.value) return
|
||||||
|
|
||||||
await bindFormRef.value.validate(async (valid) => {
|
await bindFormRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
@@ -296,15 +339,16 @@
|
|||||||
iot_card_id: bindForm.iot_card_id!,
|
iot_card_id: bindForm.iot_card_id!,
|
||||||
slot_position: bindForm.slot_position!
|
slot_position: bindForm.slot_position!
|
||||||
}
|
}
|
||||||
const res = await DeviceService.bindCard(deviceInfo.value!.id, data)
|
const res = await DeviceService.bindCard(detailData.value!.id, data)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
ElMessage.success('绑定成功')
|
ElMessage.success('绑定成功')
|
||||||
bindDialogVisible.value = false
|
bindDialogVisible.value = false
|
||||||
await loadDeviceInfo(deviceInfo.value!.id)
|
await fetchDetail(detailData.value!.id)
|
||||||
await loadDeviceCards(deviceInfo.value!.id)
|
|
||||||
if (bindFormRef.value) {
|
if (bindFormRef.value) {
|
||||||
bindFormRef.value.resetFields()
|
bindFormRef.value.resetFields()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '绑定失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -325,10 +369,9 @@
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
try {
|
try {
|
||||||
await DeviceService.unbindCard(deviceInfo.value!.id, card.iot_card_id)
|
await DeviceService.unbindCard(detailData.value!.id, card.iot_card_id)
|
||||||
ElMessage.success('解绑成功')
|
ElMessage.success('解绑成功')
|
||||||
await loadDeviceInfo(deviceInfo.value!.id)
|
await fetchDetail(detailData.value!.id)
|
||||||
await loadDeviceCards(deviceInfo.value!.id)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
ElMessage.error('解绑失败')
|
ElMessage.error('解绑失败')
|
||||||
@@ -339,14 +382,65 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回
|
onMounted(() => {
|
||||||
const handleBack = () => {
|
const deviceId = route.query.id
|
||||||
router.back()
|
const deviceNo = route.query.device_no
|
||||||
}
|
|
||||||
|
if (deviceId) {
|
||||||
|
fetchDetail(Number(deviceId))
|
||||||
|
} else if (deviceNo) {
|
||||||
|
fetchDetail(undefined, String(deviceNo))
|
||||||
|
} else {
|
||||||
|
ElMessage.error('缺少设备参数')
|
||||||
|
handleBack()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.device-detail-page {
|
.device-detail {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-section {
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -801,27 +801,14 @@
|
|||||||
remark: ''
|
remark: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 查看设备详情(通过弹窗)
|
// 跳转到设备详情页面
|
||||||
const goToDeviceSearchDetail = async (deviceNo: string) => {
|
const goToDeviceSearchDetail = (deviceNo: string) => {
|
||||||
deviceDetailDialogVisible.value = true
|
router.push({
|
||||||
deviceDetailLoading.value = true
|
path: '/asset-management/device-detail',
|
||||||
currentDeviceDetail.value = null
|
query: {
|
||||||
|
device_no: deviceNo
|
||||||
try {
|
|
||||||
const res = await DeviceService.getDeviceByImei(deviceNo)
|
|
||||||
if (res.code === 0 && res.data) {
|
|
||||||
currentDeviceDetail.value = res.data
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.message || '查询失败')
|
|
||||||
deviceDetailDialogVisible.value = false
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
})
|
||||||
console.error('查询设备详情失败:', error)
|
|
||||||
ElMessage.error(error?.message || '查询失败,请检查设备号是否正确')
|
|
||||||
deviceDetailDialogVisible.value = false
|
|
||||||
} finally {
|
|
||||||
deviceDetailLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看设备绑定的卡片
|
// 查看设备绑定的卡片
|
||||||
@@ -1012,8 +999,11 @@
|
|||||||
return h(
|
return h(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||||
onClick: () => goToDeviceSearchDetail(row.device_no)
|
onClick: (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
goToDeviceSearchDetail(row.device_no)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
row.device_no
|
row.device_no
|
||||||
)
|
)
|
||||||
|
|||||||
312
src/views/asset-management/iot-card-management/detail.vue
Normal file
312
src/views/asset-management/iot-card-management/detail.vue
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<template>
|
||||||
|
<div class="iot-card-detail-page">
|
||||||
|
<ElCard shadow="never">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="detail-header">
|
||||||
|
<ElButton @click="handleBack">
|
||||||
|
<template #icon>
|
||||||
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</ElButton>
|
||||||
|
<h2 class="detail-title">IoT卡详情</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<ElButton type="primary" @click="handleRefresh" :loading="loading">
|
||||||
|
<Icon name="refresh" /> 刷新
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading && !cardDetail" class="loading-container">
|
||||||
|
<ElIcon class="is-loading" :size="60"><Loading /></ElIcon>
|
||||||
|
<div class="loading-text">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情内容 -->
|
||||||
|
<DetailPage v-if="cardDetail" :sections="detailSections" :data="cardDetail" />
|
||||||
|
|
||||||
|
<!-- 未找到卡片 -->
|
||||||
|
<div v-if="!cardDetail && !loading" class="empty-container">
|
||||||
|
<ElEmpty description="未找到该卡片信息" />
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { CardService } from '@/api/modules'
|
||||||
|
import { ElMessage, ElIcon, ElTag } from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'IotCardDetail' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const cardDetail = ref<any>(null)
|
||||||
|
const iccid = ref<string>('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
iccid.value = (route.query.iccid as string) || ''
|
||||||
|
if (iccid.value) {
|
||||||
|
loadCardDetail()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('缺少ICCID参数')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载卡片详情
|
||||||
|
const loadCardDetail = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await CardService.getIotCardDetailByIccid(iccid.value)
|
||||||
|
if (res.code === 0) {
|
||||||
|
cardDetail.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '获取卡片详情失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取卡片详情失败:', error)
|
||||||
|
ElMessage.error('获取卡片详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
const handleRefresh = () => {
|
||||||
|
loadCardDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运营商类型文本
|
||||||
|
const getCarrierTypeText = (type: string) => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
CMCC: '中国移动',
|
||||||
|
CUCC: '中国联通',
|
||||||
|
CTCC: '中国电信',
|
||||||
|
CBN: '中国广电'
|
||||||
|
}
|
||||||
|
return typeMap[type] || type || '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡业务类型文本
|
||||||
|
const getCardCategoryText = (category: string) => {
|
||||||
|
const categoryMap: Record<string, string> = {
|
||||||
|
normal: '普通卡',
|
||||||
|
industry: '行业卡'
|
||||||
|
}
|
||||||
|
return categoryMap[category] || category || '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态文本
|
||||||
|
const getStatusText = (status: number) => {
|
||||||
|
const statusMap: Record<number, string> = {
|
||||||
|
1: '在库',
|
||||||
|
2: '已分销',
|
||||||
|
3: '已激活',
|
||||||
|
4: '已停用'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态标签类型
|
||||||
|
const getStatusTagType = (status: number) => {
|
||||||
|
const typeMap: Record<number, any> = {
|
||||||
|
1: 'info',
|
||||||
|
2: 'warning',
|
||||||
|
3: 'success',
|
||||||
|
4: 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化价格
|
||||||
|
const formatCardPrice = (price: number) => {
|
||||||
|
return `¥${((price || 0) / 100).toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetailPage 配置
|
||||||
|
const detailSections: DetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '基本信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '卡ID', prop: 'id' },
|
||||||
|
{
|
||||||
|
label: 'ICCID',
|
||||||
|
render: (data) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
padding: '3px 8px',
|
||||||
|
fontFamily: "'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace",
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: 'var(--el-text-color-regular)',
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
border: '1px solid var(--el-border-color-lighter)',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data.iccid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '卡接入号',
|
||||||
|
render: (data) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
padding: '3px 8px',
|
||||||
|
fontFamily: "'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace",
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: 'var(--el-text-color-regular)',
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
border: '1px solid var(--el-border-color-lighter)',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data.msisdn || '--'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: '运营商', prop: 'carrier_name', formatter: (value) => value || '--' },
|
||||||
|
{
|
||||||
|
label: '运营商类型',
|
||||||
|
prop: 'carrier_type',
|
||||||
|
formatter: (value) => getCarrierTypeText(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '卡业务类型',
|
||||||
|
prop: 'card_category',
|
||||||
|
formatter: (value) => getCardCategoryText(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
render: (data) => {
|
||||||
|
return h(ElTag, { type: getStatusTagType(data.status) }, () => getStatusText(data.status))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: '套餐系列', prop: 'series_name', formatter: (value) => value || '--' },
|
||||||
|
{
|
||||||
|
label: '激活状态',
|
||||||
|
render: (data) => {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{ type: data.activation_status === 1 ? 'success' : 'info' },
|
||||||
|
() => (data.activation_status === 1 ? '已激活' : '未激活')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '实名状态',
|
||||||
|
render: (data) => {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{ type: data.real_name_status === 1 ? 'success' : 'warning' },
|
||||||
|
() => (data.real_name_status === 1 ? '已实名' : '未实名')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '网络状态',
|
||||||
|
render: (data) => {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{ type: data.network_status === 1 ? 'success' : 'danger' },
|
||||||
|
() => (data.network_status === 1 ? '开机' : '停机')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '累计流量使用',
|
||||||
|
prop: 'data_usage_mb',
|
||||||
|
formatter: (value) => `${value} MB`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '一次性佣金',
|
||||||
|
render: (data) => {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{ type: data.first_commission_paid ? 'success' : 'info' },
|
||||||
|
() => (data.first_commission_paid ? '已产生' : '未产生')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '累计充值',
|
||||||
|
prop: 'accumulated_recharge',
|
||||||
|
formatter: (value) => formatCardPrice(value)
|
||||||
|
},
|
||||||
|
{ label: '所属店铺', prop: 'shop_name', formatter: (value) => value || '--' },
|
||||||
|
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },
|
||||||
|
{ label: '更新时间', prop: 'updated_at', formatter: (value) => formatDateTime(value) }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.iot-card-detail-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container {
|
||||||
|
padding: 60px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -610,6 +610,7 @@
|
|||||||
BatchSetCardSeriesBindingResponse
|
BatchSetCardSeriesBindingResponse
|
||||||
} from '@/types/api/card'
|
} from '@/types/api/card'
|
||||||
import type { PackageSeriesResponse } from '@/types/api'
|
import type { PackageSeriesResponse } from '@/types/api'
|
||||||
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
|
|
||||||
defineOptions({ name: 'StandaloneCardList' })
|
defineOptions({ name: 'StandaloneCardList' })
|
||||||
|
|
||||||
@@ -909,26 +910,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开卡详情弹窗
|
// 跳转到IoT卡详情页面
|
||||||
const goToCardDetail = async (iccid: string) => {
|
const goToCardDetail = (iccid: string) => {
|
||||||
cardDetailDialogVisible.value = true
|
if (hasAuth('iot_card:view_detail')) {
|
||||||
cardDetailLoading.value = true
|
router.push({
|
||||||
currentCardDetail.value = null
|
path: RoutesAlias.StandaloneCardList + '/detail',
|
||||||
|
query: {
|
||||||
try {
|
iccid: iccid
|
||||||
const res = await CardService.getIotCardDetailByIccid(iccid)
|
}
|
||||||
if (res.code === 0 && res.data) {
|
})
|
||||||
currentCardDetail.value = res.data
|
} else {
|
||||||
} else {
|
ElMessage.warning('您没有查看详情的权限')
|
||||||
ElMessage.error(res.message || '查询失败')
|
|
||||||
cardDetailDialogVisible.value = false
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('查询卡片详情失败:', error)
|
|
||||||
ElMessage.error(error?.message || '查询失败,请检查ICCID是否正确')
|
|
||||||
cardDetailDialogVisible.value = false
|
|
||||||
} finally {
|
|
||||||
cardDetailLoading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,8 +977,11 @@
|
|||||||
return h(
|
return h(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
|
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||||
onClick: () => goToCardDetail(row.iccid)
|
onClick: (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
goToCardDetail(row.iccid)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
row.iccid
|
row.iccid
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,58 +2,19 @@
|
|||||||
<ArtTableFullScreen>
|
<ArtTableFullScreen>
|
||||||
<div class="task-detail-page" id="table-full-screen">
|
<div class="task-detail-page" id="table-full-screen">
|
||||||
<ElCard shadow="never" class="art-table-card">
|
<ElCard shadow="never" class="art-table-card">
|
||||||
<!-- 返回按钮 -->
|
<!-- 页面头部 -->
|
||||||
<div class="back-button-wrapper">
|
<div class="detail-header">
|
||||||
<ElButton @click="goBack" icon="ArrowLeft">返回</ElButton>
|
<ElButton @click="goBack">
|
||||||
|
<template #icon>
|
||||||
|
<ElIcon><ArrowLeft /></ElIcon>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</ElButton>
|
||||||
|
<h2 class="detail-title">任务详情</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 任务基本信息 -->
|
<!-- 使用 DetailPage 组件显示任务信息 -->
|
||||||
<ElDescriptions title="任务基本信息" :column="3" border class="task-info">
|
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
||||||
<ElDescriptionsItem label="任务编号">{{ taskDetail?.task_no || '-' }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="任务类型">
|
|
||||||
<ElTag :type="taskType === 'device' ? 'warning' : 'primary'" size="small">
|
|
||||||
{{ taskType === 'device' ? '设备导入' : 'ICCID导入' }}
|
|
||||||
</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="批次号">{{ taskDetail?.batch_no || '-' }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="运营商" v-if="taskType === 'card'">
|
|
||||||
{{ (taskDetail as IotCardImportTaskDetail)?.carrier_name || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="文件名">{{ taskDetail?.file_name || '-' }}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="任务状态">
|
|
||||||
<ElTag :type="getStatusType(taskDetail?.status)" v-if="taskDetail">
|
|
||||||
{{ taskDetail.status_text }}
|
|
||||||
</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="创建时间">
|
|
||||||
{{ taskDetail?.created_at ? formatDateTime(taskDetail.created_at) : '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="开始处理时间">
|
|
||||||
{{ taskDetail?.started_at ? formatDateTime(taskDetail.started_at) : '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="完成时间">
|
|
||||||
{{ taskDetail?.completed_at ? formatDateTime(taskDetail.completed_at) : '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="错误信息" v-if="taskDetail?.error_message">
|
|
||||||
<span class="error-message">{{ taskDetail.error_message }}</span>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<!-- 统计信息 -->
|
|
||||||
<ElDescriptions title="统计信息" :column="4" border class="statistics-info">
|
|
||||||
<ElDescriptionsItem label="总数">
|
|
||||||
<span class="count-text">{{ taskDetail?.total_count || 0 }}</span>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="成功数">
|
|
||||||
<ElTag type="success">{{ taskDetail?.success_count || 0 }}</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="失败数">
|
|
||||||
<ElTag type="danger">{{ taskDetail?.fail_count || 0 }}</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="跳过数">
|
|
||||||
<ElTag type="warning">{{ taskDetail?.skip_count || 0 }}</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<!-- 失败记录 -->
|
<!-- 失败记录 -->
|
||||||
<div class="failure-section" v-if="taskDetail?.fail_count && taskDetail.fail_count > 0">
|
<div class="failure-section" v-if="taskDetail?.fail_count && taskDetail.fail_count > 0">
|
||||||
@@ -86,20 +47,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { h, computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { CardService, DeviceService } from '@/api/modules'
|
import { CardService, DeviceService } from '@/api/modules'
|
||||||
import {
|
import { ElMessage, ElTag, ElDivider, ElTable, ElTableColumn, ElIcon } from 'element-plus'
|
||||||
ElMessage,
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
ElTag,
|
|
||||||
ElDescriptions,
|
|
||||||
ElDescriptionsItem,
|
|
||||||
ElDivider,
|
|
||||||
ElTable,
|
|
||||||
ElTableColumn
|
|
||||||
} from 'element-plus'
|
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
||||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||||
|
import DetailPage from '@/components/common/DetailPage.vue'
|
||||||
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'TaskDetail' })
|
defineOptions({ name: 'TaskDetail' })
|
||||||
|
|
||||||
@@ -130,6 +87,118 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 详情页配置
|
||||||
|
const detailSections = computed((): DetailSection[] => {
|
||||||
|
const sections: DetailSection[] = [
|
||||||
|
{
|
||||||
|
title: '任务基本信息',
|
||||||
|
fields: [
|
||||||
|
{ label: '任务编号', prop: 'task_no', formatter: (value) => value || '-' },
|
||||||
|
{
|
||||||
|
label: '任务类型',
|
||||||
|
render: () => {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{ type: taskType.value === 'device' ? 'warning' : 'primary', size: 'small' },
|
||||||
|
() => (taskType.value === 'device' ? '设备导入' : 'ICCID导入')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
|
||||||
|
{ label: '文件名', prop: 'file_name', formatter: (value) => value || '-' },
|
||||||
|
{
|
||||||
|
label: '任务状态',
|
||||||
|
render: (data: TaskDetail) => {
|
||||||
|
return h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...(taskType.value === 'card'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '运营商',
|
||||||
|
prop: 'carrier_name',
|
||||||
|
formatter: (value: any) => value || '-'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
prop: 'created_at',
|
||||||
|
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '开始处理时间',
|
||||||
|
prop: 'started_at',
|
||||||
|
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '完成时间',
|
||||||
|
prop: 'completed_at',
|
||||||
|
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||||
|
},
|
||||||
|
...(taskDetail.value?.error_message
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '错误信息',
|
||||||
|
prop: 'error_message',
|
||||||
|
fullWidth: true,
|
||||||
|
render: (data: TaskDetail) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ style: { color: 'var(--el-color-danger)' } },
|
||||||
|
data.error_message || ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '统计信息',
|
||||||
|
columns: 2,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: '总数',
|
||||||
|
render: (data: TaskDetail) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'var(--el-color-primary)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
String(data.total_count || 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '成功数',
|
||||||
|
render: (data: TaskDetail) => {
|
||||||
|
return h(ElTag, { type: 'success' }, () => String(data.success_count || 0))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '失败数',
|
||||||
|
render: (data: TaskDetail) => {
|
||||||
|
return h(ElTag, { type: 'danger' }, () => String(data.fail_count || 0))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '跳过数',
|
||||||
|
render: (data: TaskDetail) => {
|
||||||
|
return h(ElTag, { type: 'warning' }, () => String(data.skip_count || 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return sections
|
||||||
|
})
|
||||||
|
|
||||||
// 返回列表
|
// 返回列表
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -181,26 +250,18 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.task-detail-page {
|
.task-detail-page {
|
||||||
.back-button-wrapper {
|
.detail-header {
|
||||||
margin-bottom: 20px;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
.task-info {
|
.detail-title {
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
}
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
.statistics-info {
|
color: var(--el-text-color-primary);
|
||||||
margin-bottom: 20px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.count-text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.failure-section,
|
.failure-section,
|
||||||
|
|||||||
495
src/views/device-management/devices/detail.vue
Normal file
495
src/views/device-management/devices/detail.vue
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
<template>
|
||||||
|
<div class="device-detail-page">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<ElPageHeader :icon="ArrowLeft" title="返回" @back="handleBack">
|
||||||
|
<template #content>
|
||||||
|
<span class="page-title">设备详情</span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<ElButton type="primary" @click="handleRefresh" :loading="loading">
|
||||||
|
<Icon name="refresh" /> 刷新
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElPageHeader>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading && !deviceDetail" class="loading-container">
|
||||||
|
<ElIcon class="is-loading" :size="60"><Loading /></ElIcon>
|
||||||
|
<div class="loading-text">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情内容 -->
|
||||||
|
<div v-else-if="deviceDetail" class="detail-content">
|
||||||
|
<!-- 设备基本信息卡片 -->
|
||||||
|
<ElCard shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="header-title">
|
||||||
|
<Icon name="mobile" /> 设备基本信息
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ElDescriptions :column="3" border>
|
||||||
|
<ElDescriptionsItem label="当前卡号">
|
||||||
|
<span class="code-text">{{ deviceDetail.currentCardNumber }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="当前运营商">
|
||||||
|
<ElTag :type="getOperatorType(deviceDetail.currentOperator)">
|
||||||
|
{{ deviceDetail.currentOperator }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="当前IMEI">
|
||||||
|
<span class="code-text">{{ deviceDetail.currentIMEI }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="设备号码">
|
||||||
|
<span class="code-text">{{ deviceDetail.deviceNumber }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备代理">
|
||||||
|
{{ deviceDetail.deviceAgent || '--' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="过期时间">
|
||||||
|
<span :class="{ 'expired-date': isExpired(deviceDetail.expiryTime) }">
|
||||||
|
{{ deviceDetail.expiryTime }}
|
||||||
|
</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="实名状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.realNameStatus)">
|
||||||
|
{{ deviceDetail.realNameStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="运营商状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.operatorStatus)">
|
||||||
|
{{ deviceDetail.operatorStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.deviceStatus)">
|
||||||
|
{{ deviceDetail.deviceStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<!-- 流量统计卡片 -->
|
||||||
|
<ElCard shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="header-title">
|
||||||
|
<Icon name="chart-bar" /> 流量统计
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="traffic-stats-grid">
|
||||||
|
<div class="stat-item total">
|
||||||
|
<div class="stat-label">总计流量</div>
|
||||||
|
<div class="stat-value">{{ deviceDetail.totalTraffic }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item used">
|
||||||
|
<div class="stat-label">已用流量</div>
|
||||||
|
<div class="stat-value">{{ deviceDetail.usedTraffic }}</div>
|
||||||
|
<ElProgress
|
||||||
|
:percentage="getUsagePercentage(deviceDetail.usedTraffic, deviceDetail.totalTraffic)"
|
||||||
|
:color="
|
||||||
|
getTrafficColor(
|
||||||
|
getUsagePercentage(deviceDetail.usedTraffic, deviceDetail.totalTraffic)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:stroke-width="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item remaining">
|
||||||
|
<div class="stat-label">剩余流量</div>
|
||||||
|
<div class="stat-value">{{ deviceDetail.remainingTraffic }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<!-- 双卡信息卡片 -->
|
||||||
|
<ElCard shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="header-title">
|
||||||
|
<Icon name="credit-card" /> 双卡信息
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="dual-cards-container">
|
||||||
|
<!-- 卡1信息 -->
|
||||||
|
<div class="card-column">
|
||||||
|
<div class="card-title">卡1信息</div>
|
||||||
|
<ElDescriptions :column="1" border>
|
||||||
|
<ElDescriptionsItem label="ICCID">
|
||||||
|
<span class="code-text">{{ deviceDetail.card1.iccid }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="接入号">
|
||||||
|
<span class="code-text">{{ deviceDetail.card1.accessNumber }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="虚拟号">
|
||||||
|
<span class="code-text">{{ deviceDetail.card1.virtualNumber }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="IMEI">
|
||||||
|
<span class="code-text">{{ deviceDetail.card1.imei }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="代理商">
|
||||||
|
{{ deviceDetail.card1.agent || '--' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="实名状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.card1.realNameStatus)">
|
||||||
|
{{ deviceDetail.card1.realNameStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="运营商">
|
||||||
|
<ElTag :type="getOperatorType(deviceDetail.card1.operator)">
|
||||||
|
{{ deviceDetail.card1.operator }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="网卡状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.card1.cardStatus)">
|
||||||
|
{{ deviceDetail.card1.cardStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="过期时间">
|
||||||
|
{{ deviceDetail.card1.expiryTime }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="套餐系列">
|
||||||
|
{{ deviceDetail.card1.packageSeries || '--' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总流量">
|
||||||
|
{{ deviceDetail.card1.totalTraffic }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="已使用流量">
|
||||||
|
<span class="traffic-info used">{{ deviceDetail.card1.usedTraffic }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="剩余流量">
|
||||||
|
<span class="traffic-info remaining">{{ deviceDetail.card1.remainingTraffic }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡2信息 -->
|
||||||
|
<div class="card-column">
|
||||||
|
<div class="card-title">卡2信息</div>
|
||||||
|
<ElDescriptions :column="1" border>
|
||||||
|
<ElDescriptionsItem label="ICCID">
|
||||||
|
<span class="code-text">{{ deviceDetail.card2.iccid }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="接入号">
|
||||||
|
<span class="code-text">{{ deviceDetail.card2.accessNumber }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="虚拟号">
|
||||||
|
<span class="code-text">{{ deviceDetail.card2.virtualNumber || '--' }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="IMEI">
|
||||||
|
<span class="code-text">{{ deviceDetail.card2.imei }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="代理商">
|
||||||
|
{{ deviceDetail.card2.agent || '--' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="实名状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.card2.realNameStatus)">
|
||||||
|
{{ deviceDetail.card2.realNameStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="运营商">
|
||||||
|
<ElTag :type="getOperatorType(deviceDetail.card2.operator)">
|
||||||
|
{{ deviceDetail.card2.operator }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="网卡状态">
|
||||||
|
<ElTag :type="getStatusType(deviceDetail.card2.cardStatus)">
|
||||||
|
{{ deviceDetail.card2.cardStatus }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="过期时间">
|
||||||
|
{{ deviceDetail.card2.expiryTime || '--' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="套餐系列">
|
||||||
|
{{ deviceDetail.card2.packageSeries || '--' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总流量">
|
||||||
|
{{ deviceDetail.card2.totalTraffic }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="已使用流量">
|
||||||
|
<span class="traffic-info used">{{ deviceDetail.card2.usedTraffic }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="剩余流量">
|
||||||
|
<span class="traffic-info remaining">{{ deviceDetail.card2.remainingTraffic }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未找到设备 -->
|
||||||
|
<ElCard v-else shadow="never" class="empty-card">
|
||||||
|
<ElEmpty description="未找到该设备信息" />
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { DeviceService } from '@/api/modules'
|
||||||
|
import { ElMessage, ElIcon } from 'element-plus'
|
||||||
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'DeviceDetail' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const deviceDetail = ref<any>(null)
|
||||||
|
const iccid = ref<string>('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
iccid.value = (route.query.iccid as string) || ''
|
||||||
|
if (iccid.value) {
|
||||||
|
loadDeviceDetail()
|
||||||
|
} else {
|
||||||
|
ElMessage.error('缺少ICCID参数')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载设备详情
|
||||||
|
const loadDeviceDetail = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.getDeviceByIccid(iccid.value)
|
||||||
|
if (res.code === 0) {
|
||||||
|
deviceDetail.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '获取设备详情失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备详情失败:', error)
|
||||||
|
ElMessage.error('获取设备详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
const handleRefresh = () => {
|
||||||
|
loadDeviceDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取运营商标签类型
|
||||||
|
const getOperatorType = (operator: string) => {
|
||||||
|
switch (operator) {
|
||||||
|
case '中国移动':
|
||||||
|
case 'gs联动1':
|
||||||
|
return 'success'
|
||||||
|
case '中国联通':
|
||||||
|
return 'primary'
|
||||||
|
case '中国电信':
|
||||||
|
case 'gs电信':
|
||||||
|
return 'warning'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusType = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case '正常':
|
||||||
|
case '已实名':
|
||||||
|
case '在线':
|
||||||
|
return 'success'
|
||||||
|
case '停机':
|
||||||
|
case '离线':
|
||||||
|
return 'danger'
|
||||||
|
case '未实名':
|
||||||
|
return 'warning'
|
||||||
|
case '接口异常':
|
||||||
|
return 'info'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否过期
|
||||||
|
const isExpired = (expiryTime: string) => {
|
||||||
|
return new Date(expiryTime) < new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算流量使用百分比
|
||||||
|
const getUsagePercentage = (used: string, total: string) => {
|
||||||
|
const usedGB = parseFloat(used.replace('GB', ''))
|
||||||
|
const totalGB = parseFloat(total.replace('GB', ''))
|
||||||
|
return Math.round((usedGB / totalGB) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取流量进度条颜色
|
||||||
|
const getTrafficColor = (percentage: number) => {
|
||||||
|
if (percentage < 50) return 'var(--el-color-success)'
|
||||||
|
if (percentage < 80) return 'var(--el-color-warning)'
|
||||||
|
return 'var(--el-color-danger)'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-detail-page {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 400px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-text {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-family: 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-info {
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.used {
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.remaining {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--el-fill-color-extra-light);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.total .stat-value {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.used .stat-value {
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.remaining .stat-value {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-cards-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.card-column {
|
||||||
|
.card-title {
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
border-bottom: 2px solid var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-card {
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-date {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-page-header) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.dual-cards-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<div class="info-label">设备号码:</div>
|
<div class="info-label">设备号码:</div>
|
||||||
<div class="info-value">
|
<div class="info-value">
|
||||||
<span class="code-text">{{ deviceInfo.deviceNumber }}</span>
|
<span class="code-text">{{ deviceInfo.deviceNumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +153,9 @@
|
|||||||
<div class="card-info-list">
|
<div class="card-info-list">
|
||||||
<div class="card-info-item">
|
<div class="card-info-item">
|
||||||
<span class="label">ICCID:</span>
|
<span class="label">ICCID:</span>
|
||||||
<span class="value code-text">{{ deviceInfo.card1.iccid }}</span>
|
<span class="value code-text clickable" @click.stop="goToDetail(deviceInfo.card1.iccid)">
|
||||||
|
{{ deviceInfo.card1.iccid }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-info-item">
|
<div class="card-info-item">
|
||||||
<span class="label">接入号:</span>
|
<span class="label">接入号:</span>
|
||||||
@@ -265,7 +267,9 @@
|
|||||||
<div class="card-info-list">
|
<div class="card-info-list">
|
||||||
<div class="card-info-item">
|
<div class="card-info-item">
|
||||||
<span class="label">ICCID:</span>
|
<span class="label">ICCID:</span>
|
||||||
<span class="value code-text">{{ deviceInfo.card2.iccid }}</span>
|
<span class="value code-text clickable" @click.stop="goToDetail(deviceInfo.card2.iccid)">
|
||||||
|
{{ deviceInfo.card2.iccid }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-info-item">
|
<div class="card-info-item">
|
||||||
<span class="label">接入号:</span>
|
<span class="label">接入号:</span>
|
||||||
@@ -755,9 +759,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceManagement' })
|
defineOptions({ name: 'DeviceManagement' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const iccidSearch = ref('')
|
const iccidSearch = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searched = ref(false)
|
const searched = ref(false)
|
||||||
@@ -1102,6 +1108,16 @@
|
|||||||
ElMessage.success(`对卡${cardNumber}执行${operationMap[operation]}操作`)
|
ElMessage.success(`对卡${cardNumber}执行${operationMap[operation]}操作`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转到设备详情页
|
||||||
|
const goToDetail = (iccid: string) => {
|
||||||
|
router.push({
|
||||||
|
path: '/asset-management/device-detail',
|
||||||
|
query: {
|
||||||
|
iccid: iccid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 订单历史数据
|
// 订单历史数据
|
||||||
const orderHistory = reactive([
|
const orderHistory = reactive([
|
||||||
{
|
{
|
||||||
@@ -1346,6 +1362,16 @@
|
|||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.expired-date {
|
.expired-date {
|
||||||
|
|||||||
Reference in New Issue
Block a user