Files
gt-agent-company/src/pages/agent-system/asset-search/index.vue
sexygoat a6961fd74f
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 1m2s
fix: ui
2026-04-25 14:46:43 +08:00

1160 lines
50 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { resolveAsset, getAssetRealtimeStatus, getCurrentPackage, getAssetPackages } from '@/api/assets'
import type { CardInfo, DeviceInfo, AssetRealtimeStatus, AssetPackage } from '@/api/assets'
import { formatTime, formatDataSize } from '@/utils/format'
// 搜索关键词
const searchKeyword = ref('')
// 资产类型
const assetType = ref<'card' | 'device' | null>(null)
// 资产信息
const assetInfo = ref<any>(null)
// 资产实时状态
const realtimeStatus = ref<AssetRealtimeStatus | null>(null)
// 当前套餐
const currentPackage = ref<AssetPackage | null>(null)
// 套餐列表
const packageList = ref<AssetPackage[]>([])
// 是否展开套餐列表
const showPackageList = ref(true)
// 是否为卡片
const isCard = computed(() => assetType.value === 'card')
// 是否为设备
const isDevice = computed(() => assetType.value === 'device')
// 卡片信息
const cardInfo = computed(() => isCard.value ? assetInfo.value : null)
// 设备信息
const deviceInfo = computed(() => isDevice.value ? assetInfo.value : null)
// 搜索资产
async function handleSearch() {
if (!searchKeyword.value.trim()) {
return
}
try {
const result = await resolveAsset(searchKeyword.value.trim())
assetType.value = result.asset_type
assetInfo.value = result
// 加载实时状态和当前套餐 (使用 identifier)
if (searchKeyword.value.trim()) {
loadRealtimeStatus(searchKeyword.value.trim())
loadCurrentPackage(searchKeyword.value.trim())
loadPackageList(searchKeyword.value.trim())
}
} catch (error : any) {
console.error('查询失败:', error)
assetType.value = null
assetInfo.value = null
realtimeStatus.value = null
}
}
// 加载实时状态
async function loadRealtimeStatus(identifier : string) {
try {
const status = await getAssetRealtimeStatus(identifier)
realtimeStatus.value = status
} catch (error : any) {
console.error('加载实时状态失败:', error)
}
}
// 加载当前套餐
async function loadCurrentPackage(identifier : string) {
try {
console.log('开始加载当前套餐:', identifier)
const pkg = await getCurrentPackage(identifier)
console.log('当前套餐数据:', pkg)
currentPackage.value = pkg
} catch (error : any) {
console.error('加载当前套餐失败:', error)
// 如果没有当前套餐404这是正常的不需要提示用户
currentPackage.value = null
}
}
// 加载套餐列表
async function loadPackageList(identifier : string) {
try {
console.log('开始加载套餐列表:', identifier)
const response = await getAssetPackages(identifier, { page: 1, page_size: 50 })
console.log('套餐列表数据:', response)
packageList.value = response?.items || []
} catch (error : any) {
console.error('加载套餐列表失败:', error)
packageList.value = []
}
}
// 切换套餐列表显示
async function togglePackageList() {
showPackageList.value = !showPackageList.value
// 第一次展开时加载套餐列表
if (showPackageList.value && packageList.value.length === 0 && searchKeyword.value.trim()) {
await loadPackageList(searchKeyword.value.trim())
}
}
// 获取卡片状态文本
function getCardStatusText(status ?: number) {
const map : Record<number, string> = {
0: '未激活',
1: '正常',
2: '停机',
3: '已停用',
}
return status !== undefined ? map[status] || '未知' : '未知'
}
// 获取实名状态文本
function getRealNameStatusText(status ?: number) {
const map : Record<number, string> = {
0: '未实名',
1: '已实名',
}
return status !== undefined ? map[status] || '未知' : '未知'
}
// 获取网络状态文本 (卡片)
function getNetworkStatusText(status ?: number) {
const map : Record<number, string> = {
0: '停机',
1: '开机',
}
return status !== undefined ? map[status] || '未知' : '未知'
}
// 获取切卡模式文本 (设备)
function getSwitchModeText(mode ?: string) {
const map : Record<string, string> = {
'0': '自动',
'1': '手动',
}
return mode ? map[mode] || mode : '自动'
}
// 格式化字节数
function formatBytes(bytes ?: string | number) {
if (!bytes) return '0B'
const num = typeof bytes === 'string' ? parseInt(bytes) : bytes
if (num < 1024) return `${num}B`
if (num < 1024 * 1024) return `${(num / 1024).toFixed(2)}KB`
if (num < 1024 * 1024 * 1024) return `${(num / 1024 / 1024).toFixed(2)}MB`
return `${(num / 1024 / 1024 / 1024).toFixed(2)}GB`
}
// 格式化时长(秒)
function formatDuration(seconds ?: string | number) {
if (!seconds) return '0秒'
const num = typeof seconds === 'string' ? parseInt(seconds) : seconds
const hours = Math.floor(num / 3600)
const minutes = Math.floor((num % 3600) / 60)
const secs = num % 60
if (hours > 0) return `${hours}小时${minutes}分钟`
if (minutes > 0) return `${minutes}分钟${secs}`
return `${secs}`
}
// 获取设备保护状态文本
function getDeviceProtectText(status ?: string | null) {
const map : Record<string, string> = {
start: '已启动',
stop: '已停止',
none: '未启用',
}
return status ? map[status] || status : '未知'
}
// 获取在线状态文本
function getOnlineStatusText(status ?: number) {
return status === 1 ? '在线' : '离线'
}
// 获取套餐状态颜色
function getPackageStatusColor(status : number) {
const map : Record<number, { bg : string, text : string }> = {
0: { bg: '#fff3e0', text: '#e65100' }, // 待生效
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 生效中
2: { bg: '#f5f5f5', text: '#999' }, // 已用完
3: { bg: '#ffebee', text: '#c62828' }, // 已过期
4: { bg: '#f5f5f5', text: '#999' }, // 已失效
}
return map[status] || { bg: '#f5f5f5', text: '#999' }
}
// 计算套餐使用百分比
function getPackageUsagePercent(pkg : AssetPackage) {
if (!pkg.virtual_limit_mb || pkg.virtual_limit_mb === 0) return 0
return Math.min(100, (pkg.virtual_used_mb / pkg.virtual_limit_mb) * 100)
}
// 获取信号质量等级 (基于 RSRP)
function getSignalQuality(rsrp ?: number) {
if (rsrp === undefined) return { level: '未知', text: '无信号', color: '#999' }
if (rsrp >= -80) {
return { level: '优秀', text: '信号极好', color: '#2e7d32' }
} else if (rsrp >= -90) {
return { level: '良好', text: '信号良好', color: '#66bb6a' }
} else if (rsrp >= -100) {
return { level: '一般', text: '信号一般', color: '#ffa726' }
} else if (rsrp >= -110) {
return { level: '较差', text: '信号较差', color: '#ff6b6b' }
} else {
return { level: '很差', text: '信号很差', color: '#d32f2f' }
}
}
// 获取信号条数1-5条
function getSignalBars(rsrp ?: number) {
if (rsrp === undefined || rsrp < -110) return 0
if (rsrp >= -80) return 5
if (rsrp >= -90) return 4
if (rsrp >= -100) return 3
if (rsrp >= -110) return 2
return 1
}
// 获取卡片状态颜色
function getCardStatusColor(status ?: number) {
const map : Record<number, { bg : string, text : string }> = {
0: { bg: '#f5f5f5', text: '#999' },
1: { bg: '#e8f5e9', text: '#2e7d32' },
2: { bg: '#ffebee', text: '#c62828' },
3: { bg: '#f5f5f5', text: '#999' },
}
return status !== undefined ? map[status] : { bg: '#f5f5f5', text: '#999' }
}
// 获取设备状态文本
function getDeviceStatusText(status ?: number) {
const map : Record<number, string> = {
0: '未激活',
1: '正常',
2: '停机',
3: '已停用',
}
return status !== undefined ? map[status] || '未知' : '未知'
}
// 获取设备状态颜色
function getDeviceStatusColor(status ?: number) {
const map : Record<number, { bg : string, text : string }> = {
0: { bg: '#f5f5f5', text: '#999' },
1: { bg: '#e8f5e9', text: '#2e7d32' },
2: { bg: '#ffebee', text: '#c62828' },
3: { bg: '#f5f5f5', text: '#999' },
}
return status !== undefined ? map[status] : { bg: '#f5f5f5', text: '#999' }
}
// 获取设备保护状态文本
function getDeviceProtectStatusText(status ?: string) {
const map : Record<string, string> = {
none: '无保护',
basic: '基础保护',
advanced: '高级保护',
}
return status ? map[status] || status : '无保护'
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
// 如果有 iccid 或 virtual_no 参数,自动搜索
if (options.iccid) {
searchKeyword.value = options.iccid
handleSearch()
} else if (options.virtual_no) {
searchKeyword.value = options.virtual_no
handleSearch()
}
})
</script>
<template>
<view class="bg-page min-h-screen pb-safe">
<!-- IoT卡详情 -->
<view v-if="isCard && cardInfo" class="px-4 pt-4 pb-4">
<!-- 卡片基本信息卡 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<!-- 顶部状态栏 -->
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<text class="text-16px font-700 text-[#212121]">
IoT卡
</text>
</view>
<view :style="{
backgroundColor: getCardStatusColor(cardInfo.status).bg,
color: getCardStatusColor(cardInfo.status).text,
}" class="px-3 py-1 rounded-8px text-12px font-600">
{{ getCardStatusText(cardInfo.status) }}
</view>
</view>
<text class="text-12px text-[#999]">
{{ cardInfo.current_package || '无套餐' }}
</text>
</view>
<!-- 卡片信息 -->
<view class="px-4 py-3">
<view class="space-y-2">
<!-- ICCID -->
<view class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">ICCID</text>
<text class="flex-1 text-13px text-[#212121] text-right break-all">
{{ cardInfo.iccid }}
</text>
</view>
<!-- 虚拟号 -->
<view v-if="cardInfo.virtual_no" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">虚拟号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ cardInfo.virtual_no }}
</text>
</view>
<!-- msisdn -->
<view v-if="cardInfo.msisdn" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">msisdn</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ cardInfo.msisdn }}
</text>
</view>
<!-- 运营商ID -->
<view v-if="cardInfo.carrier_id" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">运营商ID</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ cardInfo.carrier_id }}
</text>
</view>
<!-- 卡类型 -->
<view v-if="cardInfo.card_category" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">卡类型</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ cardInfo.card_category === 'normal' ? '普通卡' : cardInfo.card_category }}
</text>
</view>
<!-- 批次号 -->
<view v-if="cardInfo.batch_no" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">批次号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ cardInfo.batch_no }}
</text>
</view>
<!-- 网络状态 -->
<view v-if="cardInfo.network_status !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">网络状态</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ getNetworkStatusText(cardInfo.network_status) }}
</text>
</view>
<!-- 实名状态 -->
<view v-if="cardInfo.real_name_status !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">实名状态</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ getRealNameStatusText(cardInfo.real_name_status) }}
</text>
</view>
<!-- 累计充值 -->
<view v-if="cardInfo.accumulated_recharge !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-70px">累计充值</text>
<text class="flex-1 text-13px font-600 text-[var(--brand-primary)] text-right">
¥{{ cardInfo.accumulated_recharge }}
</text>
</view>
</view>
</view>
<!-- 套餐使用情况 -->
<view v-if="cardInfo.package_total_mb !== undefined && cardInfo.package_total_mb > 0" class="px-4 pb-4">
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="flex items-center justify-between mb-2">
<text class="text-12px text-[#999]">
套餐流量
</text>
<text class="text-12px font-600 text-[#212121]">
{{ formatDataSize(cardInfo.package_used_mb) }} / {{ formatDataSize(cardInfo.package_total_mb) }}
</text>
</view>
<!-- 进度条 -->
<view class="h-6px bg-[#e0e0e0] rounded-full overflow-hidden">
<view :style="{
width: `${((cardInfo.package_used_mb || 0) / cardInfo.package_total_mb) * 100}%`,
}" :class="[
'h-full rounded-full transition-all duration-300',
((cardInfo.package_used_mb || 0) / cardInfo.package_total_mb) > 0.9
? 'bg-[#ff6b6b]'
: 'bg-[#3ed268]',
]" />
</view>
</view>
<view class="flex items-center justify-between mt-2">
<text class="text-11px text-[#999]">
剩余: {{ formatDataSize(cardInfo.package_remain_mb) }}
</text>
</view>
</view>
</view>
<!-- 实时状态 -->
<view v-if="realtimeStatus && isCard" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-3">
<text class="text-13px font-600 text-[#212121] block mb-2">实时状态</text>
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="space-y-2">
<!-- 本月用量 -->
<view class="flex items-center justify-between">
<text class="text-12px text-[#666]">本月用量</text>
<text class="text-13px font-600 text-[var(--brand-primary)]">
{{
realtimeStatus.current_month_usage_mb !== null && realtimeStatus.current_month_usage_mb !== undefined
? formatDataSize(realtimeStatus.current_month_usage_mb)
: '0MB'
}}
</text>
</view>
<!-- 实时网络状态 -->
<view class="flex items-center justify-between">
<text class="text-12px text-[#666]">实时网络</text>
<view :class="[
'px-2 py-0.5 rounded text-11px font-600',
realtimeStatus.network_status === 1
? 'bg-[#e8f5e9] text-[#2e7d32]'
: 'bg-[#ffebee] text-[#c62828]',
]">
{{
realtimeStatus.network_status !== null && realtimeStatus.network_status !== undefined
? getNetworkStatusText(realtimeStatus.network_status)
: '未知'
}}
</view>
</view>
<!-- 实名状态 -->
<view class="flex items-center justify-between">
<text class="text-12px text-[#666]">实名状态</text>
<text class="text-13px text-[#212121]">
{{
realtimeStatus.real_name_status !== null && realtimeStatus.real_name_status !== undefined
? getRealNameStatusText(realtimeStatus.real_name_status)
: '未知'
}}
</text>
</view>
<!-- 最后同步时间 -->
<view v-if="realtimeStatus.last_sync_time"
class="flex items-center justify-between pt-1 border-t-1px border-[#e5e7eb]">
<text class="text-11px text-[#999]">最后同步</text>
<text class="text-11px text-[#999]">
{{ formatTime(realtimeStatus.last_sync_time) }}
</text>
</view>
</view>
</view>
</view>
<!-- 套餐信息 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-3">
<view class="flex items-center justify-between mb-2">
<text class="text-13px font-600 text-[#212121]">套餐信息</text>
<view v-if="currentPackage" class="flex items-center gap-1 px-2 py-1 rounded-8px bg-[#f5f5f5]"
@click="togglePackageList">
<text class="text-11px text-[#666]">全部套餐</text>
<i :class="[
'text-14px text-[#999] transition-transform',
showPackageList ? 'i-mdi-chevron-up' : 'i-mdi-chevron-down',
]" />
</view>
</view>
<!-- 当前套餐 -->
<view v-if="currentPackage" class="bg-[#f5f5f5] rounded-12px p-3">
<!-- 套餐名称和状态 -->
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<i :class="[
'text-16px',
currentPackage.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-14px font-600 text-[#212121]">
{{ currentPackage.package_name }}
</text>
</view>
<view :style="{
backgroundColor: getPackageStatusColor(currentPackage.status).bg,
color: getPackageStatusColor(currentPackage.status).text,
}" class="px-2 py-0.5 rounded text-10px font-600">
{{ currentPackage.status_name }}
</view>
</view>
<!-- 流量使用进度 -->
<view class="bg-white rounded-8px p-2 mb-2">
<view class="flex items-center justify-between mb-1">
<text class="text-11px text-[#666]">虚拟流量</text>
<text class="text-11px font-600 text-[#212121]">
{{ formatDataSize(currentPackage.virtual_used_mb) }} /
{{ formatDataSize(currentPackage.virtual_limit_mb) }}
</text>
</view>
<!-- 进度条 -->
<view class="h-4px bg-[#e0e0e0] rounded-full overflow-hidden">
<view :style="{ width: `${getPackageUsagePercent(currentPackage)}%` }" :class="[
'h-full rounded-full transition-all',
getPackageUsagePercent(currentPackage) > 90 ? 'bg-[#ff6b6b]' : 'bg-[#3ed268]',
]" />
</view>
</view>
<!-- 套餐详情 -->
<view class="space-y-1">
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">真实流量</text>
<text class="text-[#212121]">
{{ formatDataSize(currentPackage.data_usage_mb) }} /
{{ formatDataSize(currentPackage.data_limit_mb) }}
</text>
</view>
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">流量比例</text>
<text class="text-[#212121]">1 : {{ currentPackage.virtual_ratio }}</text>
</view>
<view v-if="currentPackage.expires_at"
class="flex items-center justify-between text-11px pt-1 border-t-1px border-[#e5e7eb]">
<text class="text-[#999]">到期时间</text>
<text class="text-[#999]">{{ formatTime(currentPackage.expires_at) }}</text>
</view>
</view>
</view>
<!-- 无套餐提示 -->
<view v-else class="bg-[#f5f5f5] rounded-12px p-4 flex flex-col items-center">
<i class="i-mdi-package-variant-closed text-40px text-[#ddd] mb-2" />
<text class="text-12px text-[#999]">暂无生效套餐</text>
</view>
<!-- 套餐列表展开时显示 -->
<view v-if="showPackageList && packageList.length > 0" class="mt-3">
<text class="text-13px font-600 text-[#212121] block mb-2">全部套餐 ({{ packageList.length }})</text>
<view class="space-y-2">
<view v-for="pkg in packageList" :key="pkg.package_usage_id" class="bg-[#f5f5f5] rounded-12px p-3">
<!-- 套餐头部 -->
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-1.5">
<i :class="[
'text-14px',
pkg.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-13px font-600 text-[#212121]">{{ pkg.package_name }}</text>
<text v-if="pkg.master_usage_id" class="text-10px text-[#999]">(加油包)</text>
</view>
<view :style="{
backgroundColor: getPackageStatusColor(pkg.status).bg,
color: getPackageStatusColor(pkg.status).text,
}" class="px-2 py-0.5 rounded text-10px font-600">
{{ pkg.status_name }}
</view>
</view>
<!-- 流量信息 -->
<view class="space-y-0.5">
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">虚拟流量</text>
<text class="text-[#212121]">
{{ formatDataSize(pkg.virtual_used_mb) }} / {{ formatDataSize(pkg.virtual_limit_mb) }}
</text>
</view>
<view v-if="pkg.activated_at" class="flex items-center justify-between text-10px">
<text class="text-[#999]">{{ formatTime(pkg.activated_at, 'date') }}</text>
<text v-if="pkg.expires_at" class="text-[#999]"> {{ formatTime(pkg.expires_at, 'date') }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 时间信息 -->
<view v-if="cardInfo.created_at" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-4">
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="flex items-center justify-between text-11px text-[#999]">
<text>创建时间: {{ formatTime(cardInfo.created_at) }}</text>
</view>
</view>
</view>
</view>
<!-- 设备详情 -->
<view v-else-if="isDevice && deviceInfo" class="px-4 pt-4 pb-4">
<!-- 设备基本信息卡 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
<!-- 顶部状态栏 -->
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<view>
<text class="text-16px font-700 text-[#212121] block mb-0.5">
{{ deviceInfo.device_name }}
</text>
<!-- 信号条和信号质量 -->
<view v-if="realtimeStatus?.device_realtime?.rsrp !== undefined" class="flex items-center gap-1.5">
<view class="flex items-end gap-0.5">
<view v-for="i in 5" :key="i" class="w-3px rounded-full transition-all"
:class="i <= getSignalBars(realtimeStatus.device_realtime.rsrp) ? 'opacity-100' : 'opacity-20'"
:style="{
height: `${i * 2 + 4}px`,
backgroundColor:
i <= getSignalBars(realtimeStatus.device_realtime.rsrp)
? getSignalQuality(realtimeStatus.device_realtime.rsrp).color
: '#ddd',
}" />
</view>
<text class="text-11px font-600"
:style="{ color: getSignalQuality(realtimeStatus.device_realtime.rsrp).color }">
{{ getSignalQuality(realtimeStatus.device_realtime.rsrp).level }}
</text>
</view>
</view>
</view>
<view :style="{
backgroundColor: getDeviceStatusColor(deviceInfo.status).bg,
color: getDeviceStatusColor(deviceInfo.status).text,
}" class="px-3 py-1 rounded-8px text-12px font-600">
{{ getDeviceStatusText(deviceInfo.status) }}
</view>
</view>
<text class="text-12px text-[#999]">
{{ deviceInfo.current_package || '无套餐' }}
</text>
</view>
<!-- 设备基本信息 -->
<view class="px-4 py-3">
<view class="space-y-2">
<!-- 虚拟号 -->
<view v-if="deviceInfo.virtual_no" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">虚拟号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.virtual_no }}
</text>
</view>
<!-- IMEI -->
<view v-if="deviceInfo.imei" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">IMEI</text>
<text class="flex-1 text-13px text-[#212121] text-right break-all">
{{ deviceInfo.imei }}
</text>
</view>
<!-- 设备型号 -->
<view v-if="deviceInfo.device_model" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">设备型号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.device_model }}
</text>
</view>
<!-- 制造商 -->
<view v-if="deviceInfo.manufacturer" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">制造商</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.manufacturer }}
</text>
</view>
<!-- 最大卡槽数 -->
<view v-if="deviceInfo.max_sim_slots" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">最大卡槽</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.max_sim_slots }}
</text>
</view>
<!-- 绑定卡数 -->
<view v-if="deviceInfo.bound_card_count !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">绑定卡数</text>
<text class="flex-1 text-13px font-600 text-[var(--brand-primary)] text-right">
{{ deviceInfo.bound_card_count }}
</text>
</view>
<!-- 批次号 -->
<view v-if="deviceInfo.batch_no" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">批次号</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.batch_no }}
</text>
</view>
<!-- 店铺信息 -->
<view v-if="deviceInfo.shop_name" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">所属店铺</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.shop_name }}
</text>
</view>
<!-- 套餐系列 -->
<view v-if="deviceInfo.series_name" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">套餐系列</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ deviceInfo.series_name }}
</text>
</view>
<!-- 保护状态 -->
<view v-if="deviceInfo.device_protect_status" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">保护状态</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ getDeviceProtectStatusText(deviceInfo.device_protect_status) }}
</text>
</view>
<!-- 切卡模式 -->
<view v-if="deviceInfo.switch_mode !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">切卡模式</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ getSwitchModeText(deviceInfo.switch_mode) }}
</text>
</view>
<!-- 实名状态 -->
<view v-if="deviceInfo.real_name_status !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">实名状态</text>
<text class="flex-1 text-13px text-[#212121] text-right">
{{ getRealNameStatusText(deviceInfo.real_name_status) }}
</text>
</view>
<!-- 累计充值 -->
<view v-if="deviceInfo.accumulated_recharge !== undefined" class="flex items-start justify-between">
<text class="text-12px text-[#999] w-80px">累计充值</text>
<text class="flex-1 text-13px font-600 text-[var(--brand-primary)] text-right">
¥{{ deviceInfo.accumulated_recharge }}
</text>
</view>
</view>
</view>
<!-- 套餐使用情况 -->
<view v-if="deviceInfo.package_total_mb !== undefined && deviceInfo.package_total_mb > 0" class="px-4 pb-4">
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="flex items-center justify-between mb-2">
<text class="text-12px text-[#999]">
套餐流量
</text>
<text class="text-12px font-600 text-[#212121]">
{{ formatDataSize(deviceInfo.package_used_mb) }} / {{ formatDataSize(deviceInfo.package_total_mb) }}
</text>
</view>
<!-- 进度条 -->
<view class="h-6px bg-[#e0e0e0] rounded-full overflow-hidden">
<view :style="{
width: `${((deviceInfo.package_used_mb || 0) / deviceInfo.package_total_mb) * 100}%`,
}" :class="[
'h-full rounded-full transition-all duration-300',
((deviceInfo.package_used_mb || 0) / deviceInfo.package_total_mb) > 0.9
? 'bg-[#ff6b6b]'
: 'bg-[#3ed268]',
]" />
</view>
</view>
<view class="flex items-center justify-between mt-2">
<text class="text-11px text-[#999]">
剩余: {{ formatDataSize(deviceInfo.package_remain_mb) }}
</text>
</view>
</view>
</view>
<!-- 绑定卡片列表 -->
<view v-if="deviceInfo.cards && deviceInfo.cards.length > 0" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-3">
<text class="text-13px font-600 text-[#212121] block mb-2">绑定IoT卡</text>
<view class="space-y-2">
<view v-for="card in deviceInfo.cards" :key="card.card_id" class="bg-[#f5f5f5] rounded-12px p-3">
<view class="flex items-center justify-between mb-1">
<text class="text-12px text-[#212121]">卡槽 {{ card.slot_position }}</text>
<text v-if="card.is_current" class="text-11px text-white bg-[#3ed268] px-2 py-0.5 rounded">
当前
</text>
</view>
<view class="space-y-1">
<text class="text-11px text-[#666] block">ICCID: {{ card.iccid }}</text>
<text class="text-11px text-[#666] block">电话: {{ card.msisdn }}</text>
<view class="flex items-center justify-between">
<text class="text-11px text-[#666]">
网络: {{ getNetworkStatusText(card.network_status) }}
</text>
<text class="text-11px text-[#666]">
实名: {{ getRealNameStatusText(card.real_name_status) }}
</text>
</view>
</view>
</view>
</view>
</view>
<!-- 设备实时状态 -->
<view v-if="realtimeStatus?.device_realtime" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-3">
<text class="text-13px font-600 text-[#212121] block mb-2">设备实时状态</text>
<!-- 在线状态 -->
<view class="bg-[#f5f5f5] rounded-12px p-3 mb-2">
<view class="flex items-center justify-between mb-2">
<text class="text-12px text-[#666]">在线状态</text>
<view :class="[
'px-2 py-0.5 rounded text-11px font-600',
realtimeStatus.device_realtime.online_status === 1 ? 'bg-[#e8f5e9] text-[#2e7d32]' : 'bg-[#f5f5f5] text-[#999]',
]">
{{ getOnlineStatusText(realtimeStatus.device_realtime.online_status) }}
</view>
</view>
<view class="space-y-1">
<!-- 电池电量 -->
<view v-if="realtimeStatus.device_realtime.battery_level !== undefined"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">电池电量</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.battery_level }}%</text>
</view>
<!-- 连接客户端 -->
<view v-if="realtimeStatus.device_realtime.client_number !== undefined"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">连接客户端</text>
<text class="text-[#212121]">
{{ realtimeStatus.device_realtime.client_number }} /
{{ realtimeStatus.device_realtime.max_clients }}
</text>
</view>
<!-- 连接时长 -->
<view v-if="realtimeStatus.device_realtime.connect_time"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">连接时长</text>
<text class="text-[#212121]">{{ formatDuration(realtimeStatus.device_realtime.connect_time) }}</text>
</view>
<!-- 运行时长 -->
<view v-if="realtimeStatus.device_realtime.run_time" class="flex items-center justify-between text-11px">
<text class="text-[#666]">运行时长</text>
<text class="text-[#212121]">{{ formatDuration(realtimeStatus.device_realtime.run_time) }}</text>
</view>
</view>
</view>
<!-- 流量统计 -->
<view class="bg-[#f5f5f5] rounded-12px p-3 mb-2">
<text class="text-12px text-[#666] block mb-2">流量统计</text>
<view class="space-y-1">
<!-- 今日用量 -->
<view v-if="realtimeStatus.device_realtime.daily_usage" class="flex items-center justify-between text-11px">
<text class="text-[#666]">今日用量</text>
<text class="text-[var(--brand-primary)] font-600">{{ formatBytes(realtimeStatus.device_realtime.daily_usage) }}</text>
</view>
<!-- 下载流量 -->
<view v-if="realtimeStatus.device_realtime.dl_stats" class="flex items-center justify-between text-11px">
<text class="text-[#666]">下载流量</text>
<text class="text-[#212121]">{{ formatBytes(realtimeStatus.device_realtime.dl_stats) }}</text>
</view>
<!-- 上传流量 -->
<view v-if="realtimeStatus.device_realtime.ul_stats" class="flex items-center justify-between text-11px">
<text class="text-[#666]">上传流量</text>
<text class="text-[#212121]">{{ formatBytes(realtimeStatus.device_realtime.ul_stats) }}</text>
</view>
<!-- 限速 -->
<view v-if="realtimeStatus.device_realtime.limit_speed" class="flex items-center justify-between text-11px">
<text class="text-[#666]">限速</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.limit_speed }} Mbps</text>
</view>
</view>
</view>
<!-- 网络信息 -->
<view class="bg-[#f5f5f5] rounded-12px p-3 mb-2">
<text class="text-12px text-[#666] block mb-2">网络信息</text>
<view class="space-y-1">
<!-- IP地址 -->
<view v-if="realtimeStatus.device_realtime.ip_address" class="flex items-center justify-between text-11px">
<text class="text-[#666]">IP地址</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.ip_address }}</text>
</view>
<!-- LAN IP -->
<view v-if="realtimeStatus.device_realtime.lan_ip" class="flex items-center justify-between text-11px">
<text class="text-[#666]">LAN IP</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.lan_ip }}</text>
</view>
<!-- WAN IP -->
<view v-if="realtimeStatus.device_realtime.wan_ip" class="flex items-center justify-between text-11px">
<text class="text-[#666]">WAN IP</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.wan_ip }}</text>
</view>
<!-- MAC地址 -->
<view v-if="realtimeStatus.device_realtime.mac_address" class="flex items-center justify-between text-11px">
<text class="text-[#666]">MAC地址</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.mac_address }}</text>
</view>
</view>
</view>
<!-- WiFi信息 -->
<view v-if="realtimeStatus.device_realtime.ssid" class="bg-[#f5f5f5] rounded-12px p-3 mb-2">
<text class="text-12px text-[#666] block mb-2">WiFi信息</text>
<view class="space-y-1">
<!-- SSID -->
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">SSID</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.ssid }}</text>
</view>
<!-- WiFi状态 -->
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">WiFi状态</text>
<text :class="realtimeStatus.device_realtime.wifi_enabled ? 'text-[#2e7d32]' : 'text-[#999]'">
{{ realtimeStatus.device_realtime.wifi_enabled ? '已开启' : '已关闭' }}
</text>
</view>
<!-- WiFi密码 -->
<view v-if="realtimeStatus.device_realtime.wifi_password"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">WiFi密码</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.wifi_password }}</text>
</view>
</view>
</view>
<!-- 其他信息 -->
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="space-y-1">
<!-- 软件版本 -->
<view v-if="realtimeStatus.device_realtime.software_version"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">软件版本</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.software_version }}</text>
</view>
<!-- IMSI -->
<view v-if="realtimeStatus.device_realtime.imsi" class="flex items-center justify-between text-11px">
<text class="text-[#666]">IMSI</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.imsi }}</text>
</view>
<!-- 当前ICCID -->
<view v-if="realtimeStatus.device_realtime.current_iccid"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">当前ICCID</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.current_iccid }}</text>
</view>
<!-- 同步间隔 -->
<view v-if="realtimeStatus.device_realtime.sync_interval"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">同步间隔</text>
<text class="text-[#212121]">{{ realtimeStatus.device_realtime.sync_interval }}</text>
</view>
<!-- 最后更新 -->
<view v-if="realtimeStatus.device_realtime.last_update_time"
class="flex items-center justify-between text-11px">
<text class="text-[#666]">最后更新</text>
<text class="text-[#999]">{{ formatTime(realtimeStatus.device_realtime.last_update_time) }}</text>
</view>
</view>
</view>
</view>
<!-- 套餐信息 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-3">
<view class="flex items-center justify-between mb-2">
<text class="text-13px font-600 text-[#212121]">套餐信息</text>
<view v-if="currentPackage" class="flex items-center gap-1 px-2 py-1 rounded-8px bg-[#f5f5f5]"
@click="togglePackageList">
<text class="text-11px text-[#666]">全部套餐</text>
<i :class="[
'text-14px text-[#999] transition-transform',
showPackageList ? 'i-mdi-chevron-up' : 'i-mdi-chevron-down',
]" />
</view>
</view>
<!-- 当前套餐 -->
<view v-if="currentPackage" class="bg-[#f5f5f5] rounded-12px p-3">
<!-- 套餐名称和状态 -->
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-2">
<i :class="[
'text-16px',
currentPackage.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-14px font-600 text-[#212121]">
{{ currentPackage.package_name }}
</text>
</view>
<view :style="{
backgroundColor: getPackageStatusColor(currentPackage.status).bg,
color: getPackageStatusColor(currentPackage.status).text,
}" class="px-2 py-0.5 rounded text-10px font-600">
{{ currentPackage.status_name }}
</view>
</view>
<!-- 流量使用进度 -->
<view class="bg-white rounded-8px p-2 mb-2">
<view class="flex items-center justify-between mb-1">
<text class="text-11px text-[#666]">虚拟流量</text>
<text class="text-11px font-600 text-[#212121]">
{{ formatDataSize(currentPackage.virtual_used_mb) }} /
{{ formatDataSize(currentPackage.virtual_limit_mb) }}
</text>
</view>
<!-- 进度条 -->
<view class="h-4px bg-[#e0e0e0] rounded-full overflow-hidden">
<view :style="{ width: `${getPackageUsagePercent(currentPackage)}%` }" :class="[
'h-full rounded-full transition-all',
getPackageUsagePercent(currentPackage) > 90 ? 'bg-[#ff6b6b]' : 'bg-[#3ed268]',
]" />
</view>
</view>
<!-- 套餐详情 -->
<view class="space-y-1">
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">真实流量</text>
<text class="text-[#212121]">
{{ formatDataSize(currentPackage.data_usage_mb) }} /
{{ formatDataSize(currentPackage.data_limit_mb) }}
</text>
</view>
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">流量比例</text>
<text class="text-[#212121]">1 : {{ currentPackage.virtual_ratio }}</text>
</view>
<view v-if="currentPackage.expires_at"
class="flex items-center justify-between text-11px pt-1 border-t-1px border-[#e5e7eb]">
<text class="text-[#999]">到期时间</text>
<text class="text-[#999]">{{ formatTime(currentPackage.expires_at) }}</text>
</view>
</view>
</view>
<!-- 无套餐提示 -->
<view v-else class="bg-[#f5f5f5] rounded-12px p-4 flex flex-col items-center">
<i class="i-mdi-package-variant-closed text-40px text-[#ddd] mb-2" />
<text class="text-12px text-[#999]">暂无生效套餐</text>
</view>
<!-- 套餐列表展开时显示 -->
<view v-if="showPackageList && packageList.length > 0" class="mt-3">
<text class="text-13px font-600 text-[#212121] block mb-2">全部套餐 ({{ packageList.length }})</text>
<view class="space-y-2">
<view v-for="pkg in packageList" :key="pkg.package_usage_id" class="bg-[#f5f5f5] rounded-12px p-3">
<!-- 套餐头部 -->
<view class="flex items-center justify-between mb-2">
<view class="flex items-center gap-1.5">
<i :class="[
'text-14px',
pkg.package_type === 'formal' ? 'i-mdi-package-variant text-[var(--brand-primary)]' : 'i-mdi-gas-station text-[#2e7d32]',
]" />
<text class="text-13px font-600 text-[#212121]">{{ pkg.package_name }}</text>
<text v-if="pkg.master_usage_id" class="text-10px text-[#999]">(加油包)</text>
</view>
<view :style="{
backgroundColor: getPackageStatusColor(pkg.status).bg,
color: getPackageStatusColor(pkg.status).text,
}" class="px-2 py-0.5 rounded text-10px font-600">
{{ pkg.status_name }}
</view>
</view>
<!-- 流量信息 -->
<view class="space-y-0.5">
<view class="flex items-center justify-between text-11px">
<text class="text-[#666]">虚拟流量</text>
<text class="text-[#212121]">
{{ formatDataSize(pkg.virtual_used_mb) }} / {{ formatDataSize(pkg.virtual_limit_mb) }}
</text>
</view>
<view v-if="pkg.activated_at" class="flex items-center justify-between text-10px">
<text class="text-[#999]">{{ formatTime(pkg.activated_at, 'date') }}</text>
<text v-if="pkg.expires_at" class="text-[#999]"> {{ formatTime(pkg.expires_at, 'date') }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 时间信息 -->
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mt-2 p-2 px-4 pb-4">
<view class="bg-[#f5f5f5] rounded-12px p-3">
<view class="space-y-1">
<view v-if="deviceInfo.created_at" class="flex items-center justify-between text-11px">
<text class="text-[#999]">创建时间</text>
<text class="text-[#212121]">{{ formatTime(deviceInfo.created_at) }}</text>
</view>
<view v-if="deviceInfo.last_online_time" class="flex items-center justify-between text-11px">
<text class="text-[#999]">最后在线</text>
<text class="text-[#212121]">{{ formatTime(deviceInfo.last_online_time) }}</text>
</view>
<view v-if="deviceInfo.last_gateway_sync_at" class="flex items-center justify-between text-11px">
<text class="text-[#999]">网关同步</text>
<text class="text-[#212121]">{{ formatTime(deviceInfo.last_gateway_sync_at) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="px-4 pt-20 flex flex-col items-center">
<i class="i-mdi-package-variant-closed text-6xl text-[#cbd5e1] mb-3" />
<text class="text-base text-[#64748b]">暂无数据</text>
</view>
<!-- 底部留白 -->
<view class="h-8" />
</view>
</template>
<style scoped>
.space-y-2>view:not(:last-child) {
margin-bottom: 8px;
}
.break-all {
word-break: break-all;
}
</style>