1373 lines
62 KiB
Vue
1373 lines
62 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { resolveAsset, getAssetRealtimeStatus, getCurrentPackage, getAssetPackages } from '@/api/assets'
|
||
import type { AssetRealtimeStatus, AssetPackage, AssetResolveResponse } from '@/api/assets'
|
||
import { formatTime, formatDataSize } from '@/utils/format'
|
||
import { formatPackageDataSize, formatPackagePrice, getPackageRemainMb, getPackageTotalMb, getPackageUsagePercent, getPackageUsedMb } from '../package-display'
|
||
|
||
// 搜索关键词
|
||
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)
|
||
type DeviceBoundCard = NonNullable<AssetResolveResponse['cards']>[number]
|
||
const deviceBoundCards = computed<DeviceBoundCard[]>(() => Array.isArray(deviceInfo.value?.cards) ? deviceInfo.value.cards : [])
|
||
const activeDeviceCard = computed<DeviceBoundCard | null>(() => {
|
||
if (deviceBoundCards.value.length === 1) {
|
||
return deviceBoundCards.value[0]
|
||
}
|
||
|
||
if (deviceBoundCards.value.length > 1) {
|
||
return deviceBoundCards.value.find(card => card.is_current) ?? null
|
||
}
|
||
|
||
return null
|
||
})
|
||
const deviceRealNameStatus = computed(() => activeDeviceCard.value?.real_name_status ?? deviceInfo.value?.real_name_status)
|
||
|
||
// 搜索资产
|
||
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
|
||
currentPackage.value = null
|
||
packageList.value = []
|
||
}
|
||
}
|
||
|
||
// 加载实时状态
|
||
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 getRealNameStatusTagClass(status ?: number | null) {
|
||
return status === 1
|
||
? 'bg-[#e8f5e9] text-[#2e7d32]'
|
||
: 'bg-[#fff1f0] text-[#cf1322]'
|
||
}
|
||
|
||
// 获取网络状态文本 (卡片)
|
||
function getNetworkStatusText(status ?: number) {
|
||
const map : Record<number, string> = {
|
||
0: '停机',
|
||
1: '正常',
|
||
}
|
||
return status !== undefined ? map[status] || '未知' : '未知'
|
||
}
|
||
|
||
// 格式化字节数
|
||
function formatBytes(bytes ?: string | number | null) {
|
||
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 isEmptyDisplayValue(value ?: string | number | null) {
|
||
return value === null || value === undefined || (typeof value === 'string' && value.trim() === '')
|
||
}
|
||
|
||
function getDisplayValue(value ?: string | number | null) {
|
||
return isEmptyDisplayValue(value) ? '-' : String(value)
|
||
}
|
||
|
||
function formatBytesDisplay(bytes ?: string | number | null) {
|
||
return isEmptyDisplayValue(bytes) ? '-' : formatBytes(bytes)
|
||
}
|
||
|
||
// 格式化时长(秒)
|
||
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 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 getPackageTypeText(type ?: AssetPackage['package_type']) {
|
||
const map : Record<string, string> = {
|
||
formal: '正式套餐',
|
||
addon: '加油包',
|
||
}
|
||
return type ? map[type] || type : '-'
|
||
}
|
||
|
||
function formatPackageEffectivePeriod(activatedAt ?: string | null, expiresAt ?: string | null) {
|
||
return `生效期:${formatTime(activatedAt || undefined, 'date')} 至 ${formatTime(expiresAt || undefined, 'date')}`
|
||
}
|
||
|
||
function formatCurrentPackagePeriodRange(activatedAt ?: string | null, expiresAt ?: string | null) {
|
||
return `生效期:${formatTime(activatedAt || undefined, 'date')} - ${formatTime(expiresAt || undefined, 'date')}`
|
||
}
|
||
|
||
function getDeviceLastOnlineTime() {
|
||
const lastOnlineTime = realtimeStatus.value?.device_realtime?.last_online_time || deviceInfo.value?.last_online_time
|
||
return lastOnlineTime ? formatTime(lastOnlineTime) : '-'
|
||
}
|
||
|
||
// 获取信号质量等级 (基于 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' }
|
||
}
|
||
|
||
onMounted(() => {
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1] as any
|
||
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 v-if="false" 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>
|
||
|
||
<!-- 卡类型 -->
|
||
<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.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
|
||
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="false && 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-start justify-between gap-3 mb-3">
|
||
<view class="min-w-0 flex-1">
|
||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||
{{ currentPackage.package_name || '-' }}
|
||
</text>
|
||
<text class="text-11px text-[#64748b] block mt-1 break-all">
|
||
{{ formatCurrentPackagePeriodRange(currentPackage.activated_at, currentPackage.expires_at) }}
|
||
</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="grid grid-cols-3 gap-2 mb-3">
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">总量</text>
|
||
<text class="text-12px font-600 text-[#212121]">{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">已使用</text>
|
||
<text class="text-12px font-600 text-[var(--brand-primary)]">{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">剩余</text>
|
||
<text class="text-12px font-600 text-[#2e7d32]">{{ formatPackageDataSize(getPackageRemainMb(currentPackage)) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bg-white rounded-8px p-2 mb-3">
|
||
<view class="flex items-center justify-between mb-1">
|
||
<text class="text-11px text-[#666]">已使用</text>
|
||
<text class="text-11px font-600 text-[#212121]">
|
||
{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }} /
|
||
{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}
|
||
</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 mb-1">
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐价格</text>
|
||
<text class="text-[#212121]">{{ formatPackagePrice(currentPackage.paid_amount) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐类型</text>
|
||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">下单时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="false" class="flex items-start justify-between gap-3 mb-3">
|
||
<view class="min-w-0 flex-1">
|
||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||
{{ currentPackage.package_name || '-' }}
|
||
</text>
|
||
<text class="text-11px text-[#999] block mt-1 break-all">
|
||
订单号:{{ currentPackage.order_no || '-' }}
|
||
</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 v-if="false" class="grid grid-cols-3 gap-2 mb-3">
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">总量</text>
|
||
<text class="text-12px font-600 text-[#212121]">{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">已使用</text>
|
||
<text class="text-12px font-600 text-[var(--brand-primary)]">{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">剩余</text>
|
||
<text class="text-12px font-600 text-[#2e7d32]">{{ formatPackageDataSize(getPackageRemainMb(currentPackage)) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="false" class="bg-white rounded-8px p-2 mb-3">
|
||
<view class="flex items-center justify-between mb-1">
|
||
<text class="text-11px text-[#666]">已使用</text>
|
||
<text class="text-11px font-600 text-[#212121]">
|
||
{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }} /
|
||
{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}
|
||
</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 v-if="false" class="space-y-1 mb-1">
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐价格</text>
|
||
<text class="text-[#212121]">{{ formatPackagePrice(currentPackage.paid_amount) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐类型</text>
|
||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">开始时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.activated_at) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">到期时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.expires_at) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">下单时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="false" class="flex items-start justify-between gap-3 mb-3">
|
||
<view class="min-w-0 flex-1">
|
||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||
{{ currentPackage.package_name || '-' }}
|
||
</text>
|
||
<text class="text-11px text-[#999] block mt-1 break-all">
|
||
订单号:{{ currentPackage.order_no || '-' }}
|
||
</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 v-if="false" class="grid grid-cols-3 gap-2 mb-3">
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">总量</text>
|
||
<text class="text-12px font-600 text-[#212121]">{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">已使用</text>
|
||
<text class="text-12px font-600 text-[var(--brand-primary)]">{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">剩余</text>
|
||
<text class="text-12px font-600 text-[#2e7d32]">{{ formatPackageDataSize(getPackageRemainMb(currentPackage)) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="false" class="bg-white rounded-8px p-2 mb-3">
|
||
<view class="flex items-center justify-between mb-1">
|
||
<text class="text-11px text-[#666]">已使用</text>
|
||
<text class="text-11px font-600 text-[#212121]">
|
||
{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }} /
|
||
{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}
|
||
</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 v-if="false" class="space-y-1 mb-1">
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐价格</text>
|
||
<text class="text-[#212121]">{{ formatPackagePrice(currentPackage.paid_amount) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐类型</text>
|
||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">开始时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.activated_at) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">到期时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.expires_at) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">下单时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 套餐名称和状态 -->
|
||
<view v-if="false" 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 v-if="false" 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>
|
||
|
||
<!-- 无套餐提示 -->
|
||
<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="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]">
|
||
{{ formatPackageDataSize(getPackageUsedMb(pkg)) }} / {{ formatPackageDataSize(getPackageTotalMb(pkg)) }}
|
||
</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">剩余</text>
|
||
<text class="text-[#212121]">{{ formatPackageDataSize(getPackageRemainMb(pkg)) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">金额</text>
|
||
<text class="text-[#212121]">{{ formatPackagePrice(pkg.paid_amount) }}</text>
|
||
</view>
|
||
<view
|
||
v-if="pkg.status === 1"
|
||
class="mt-2 flex items-start gap-2 rounded-10px border border-[#ffd591] bg-[#fff7e6] px-3 py-2"
|
||
>
|
||
<i class="i-mdi-calendar-clock mt-2px shrink-0 text-14px text-[#d46b08]" />
|
||
<text class="text-12px font-700 leading-5 text-[#ad4e00]">
|
||
{{ formatPackageEffectivePeriod(pkg.activated_at, pkg.expires_at) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</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 v-if="false" 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.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.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="deviceRealNameStatus !== 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(deviceRealNameStatus) }}
|
||
</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="deviceBoundCards.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 deviceBoundCards" :key="card.card_id" class="bg-[#f5f5f5] rounded-12px p-3">
|
||
<view class="flex items-center justify-between mb-1">
|
||
<view class="flex items-center gap-2 min-w-0">
|
||
<text class="text-12px text-[#212121]">卡槽 {{ card.slot_position }}</text>
|
||
<text :class="[
|
||
'px-2 py-0.5 rounded-full text-11px font-600',
|
||
getRealNameStatusTagClass(card.real_name_status),
|
||
]">
|
||
{{ getRealNameStatusText(card.real_name_status) }}
|
||
</text>
|
||
</view>
|
||
<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">MSISDN: {{ card.msisdn || '-' }}</text>
|
||
<text class="text-11px text-[#666] block">运营商: {{ card.carrier_name || '-' }}</text>
|
||
<view class="flex items-center justify-between">
|
||
<text class="text-11px text-[#666]">
|
||
网络: {{ getNetworkStatusText(card.network_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 class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">最后上线时间</text>
|
||
<text class="text-[#212121]">{{ getDeviceLastOnlineTime() }}</text>
|
||
</view>
|
||
|
||
<!-- 信号 -->
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">信号</text>
|
||
<text class="text-[#212121]">{{ getDisplayValue(realtimeStatus.device_realtime.signal_quality) }}</text>
|
||
</view>
|
||
|
||
<!-- 信号差原因 -->
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">信号差原因</text>
|
||
<text class="text-[#212121]">{{ getDisplayValue(realtimeStatus.device_realtime.signal_bad_reason) }}</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 class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">今日用量</text>
|
||
<text class="text-[var(--brand-primary)] font-600">{{ formatBytesDisplay(realtimeStatus.device_realtime.daily_usage) }}</text>
|
||
</view>
|
||
|
||
<!-- 限速 -->
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">限速</text>
|
||
<text class="text-[#212121]">
|
||
{{ isEmptyDisplayValue(realtimeStatus.device_realtime.limit_speed) ? '-' : `${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">
|
||
<!-- WiFi名称 -->
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">WiFi名称</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="false && 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-start justify-between gap-3 mb-3">
|
||
<view class="min-w-0 flex-1">
|
||
<text class="text-14px font-600 text-[#212121] block break-all">
|
||
{{ currentPackage.package_name || '-' }}
|
||
</text>
|
||
<text class="text-11px text-[#64748b] block mt-1 break-all">
|
||
{{ formatCurrentPackagePeriodRange(currentPackage.activated_at, currentPackage.expires_at) }}
|
||
</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="grid grid-cols-3 gap-2 mb-3">
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">总量</text>
|
||
<text class="text-12px font-600 text-[#212121]">{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">已使用</text>
|
||
<text class="text-12px font-600 text-[var(--brand-primary)]">{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }}</text>
|
||
</view>
|
||
<view class="bg-white rounded-8px p-2 text-center">
|
||
<text class="text-10px text-[#999] block mb-1">剩余</text>
|
||
<text class="text-12px font-600 text-[#2e7d32]">{{ formatPackageDataSize(getPackageRemainMb(currentPackage)) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bg-white rounded-8px p-2 mb-3">
|
||
<view class="flex items-center justify-between mb-1">
|
||
<text class="text-11px text-[#666]">已使用</text>
|
||
<text class="text-11px font-600 text-[#212121]">
|
||
{{ formatPackageDataSize(getPackageUsedMb(currentPackage)) }} /
|
||
{{ formatPackageDataSize(getPackageTotalMb(currentPackage)) }}
|
||
</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 mb-1">
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐价格</text>
|
||
<text class="text-[#212121]">{{ formatPackagePrice(currentPackage.paid_amount) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">套餐类型</text>
|
||
<text class="text-[#212121]">{{ getPackageTypeText(currentPackage.package_type) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">下单时间</text>
|
||
<text class="text-[#212121]">{{ formatTime(currentPackage.created_at) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 套餐名称和状态 -->
|
||
<view v-if="false" 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 v-if="false" 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>
|
||
|
||
<!-- 无套餐提示 -->
|
||
<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="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]">
|
||
{{ formatPackageDataSize(getPackageUsedMb(pkg)) }} / {{ formatPackageDataSize(getPackageTotalMb(pkg)) }}
|
||
</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">剩余</text>
|
||
<text class="text-[#212121]">{{ formatPackageDataSize(getPackageRemainMb(pkg)) }}</text>
|
||
</view>
|
||
<view class="flex items-center justify-between text-11px">
|
||
<text class="text-[#666]">金额</text>
|
||
<text class="text-[#212121]">{{ formatPackagePrice(pkg.paid_amount) }}</text>
|
||
</view>
|
||
<view
|
||
v-if="pkg.status === 1"
|
||
class="mt-2 flex items-start gap-2 rounded-10px border border-[#ffd591] bg-[#fff7e6] px-3 py-2"
|
||
>
|
||
<i class="i-mdi-calendar-clock mt-2px shrink-0 text-14px text-[#d46b08]" />
|
||
<text class="text-12px font-700 leading-5 text-[#ad4e00]">
|
||
{{ formatPackageEffectivePeriod(pkg.activated_at, pkg.expires_at) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</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>
|