Files
one-pipe-system/src/views/asset-management/asset-information/components/BasicInfoCard.vue
luo 47a2d88c9d
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m41s
复机实名校验提示适配
2026-07-21 14:30:30 +08:00

1057 lines
36 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.
<template>
<ElCard shadow="never" class="info-card basic-info">
<template #header>
<div class="card-header">
<div class="card-header-left">
<span>{{ cardInfo?.asset_type === 'card' ? '卡信息' : '设备信息' }}</span>
<ElTag :type="cardInfo?.asset_type === 'card' ? 'success' : 'primary'" size="small">
{{ cardInfo?.asset_type === 'card' ? 'IoT卡' : '设备' }}
</ElTag>
</div>
<div class="card-header-right">
<ElTooltip content="开启后系统将自动轮询更新资产状态" placement="top">
<div v-if="canShowPolling" class="polling-switch-wrapper">
<span class="polling-label">自动轮询</span>
<ElSwitch
:model-value="pollingEnabled"
@update:model-value="handlePollingChange"
active-text=""
inactive-text=""
inline-prompt
size="default"
/>
</div>
</ElTooltip>
</div>
</div>
</template>
<ElDescriptions :column="descriptionsColumn" border>
<!-- 卡专属字段 -->
<template v-if="cardInfo?.asset_type === 'card'">
<ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="IMEI">{{
cardInfo?.gateway_card_imei || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商账户">{{ carrierAccountDisplay }}</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag :type="getRealNameStatusType(cardInfo?.real_name_status)" size="small">
{{ getRealNameStatusName(cardInfo?.real_name_status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem v-if="canShowSensitiveFields" label="实名认证策略">
{{ getRealnamePolicyName(cardInfo?.realname_policy) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="店铺">{{ cardInfo?.shop_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="套餐系列">{{ cardInfo?.series_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="虚拟号">{{ cardInfo?.virtual_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="网络状态">
<ElTag :type="cardInfo?.network_status === 1 ? 'success' : 'danger'" size="small">
{{ cardInfo?.network_status === 1 ? '正常' : '停机' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem v-if="canShowCardMonthUsage" label="本月已用流量">{{
formatDataSize(cardInfo?.current_month_usage_mb || 0)
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="canShowCardMonthUsage" label="运营商周期月内已用流量">{{
formatDataSize(cardInfo?.last_gateway_reading_mb || 0)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="实名时间">{{
cardInfo?.real_name_at ? formatDateTime(cardInfo.real_name_at) : '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="实名同步时间">{{
formatDateTime(cardInfo?.last_real_name_check_at) || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="MSISDN">{{ cardInfo?.msisdn || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="资产状态">
<ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">
{{ getAssetStatusName(cardInfo?.status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="卡状态同步时间">{{
formatDateTime(cardInfo?.last_card_status_check_at) || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="流量同步时间">{{
formatDateTime(cardInfo?.last_data_check_at) || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商状态" :span="1">
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="所有套餐已用量">
{{ formatUsageSummaryMb(cardInfo?.total_virtual_used_mb) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="所有套餐剩余量">
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
</ElDescriptionsItem>
<template v-if="cardInfo?.bound_device_id">
<ElDescriptionsItem label="绑定设备号">
<ElButton
type="primary"
link
@click="emit('navigateToDevice', cardInfo!.bound_device_no!)"
>
{{ cardInfo?.bound_device_no || '-' }}
</ElButton>
</ElDescriptionsItem>
<ElDescriptionsItem label="绑定设备名称">
{{ cardInfo?.bound_device_name || '-' }}
</ElDescriptionsItem>
</template>
</template>
<!-- 设备专属字段 -->
<template v-if="cardInfo?.asset_type === 'device'">
<ElDescriptionsItem label="IMEI">{{ cardInfo?.imei || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备名称">{{ cardInfo?.device_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="店铺">{{ cardInfo?.shop_name || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="设备状态">
<ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">
{{ getAssetStatusName(cardInfo?.status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="实名状态">
<ElTag
v-if="selectedDeviceStatusCard"
:type="getRealNameStatusType(selectedDeviceStatusCard.real_name_status)"
size="small"
>
{{ getRealNameStatusName(selectedDeviceStatusCard.real_name_status) }}
</ElTag>
<span v-else>-</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="激活状态">
{{ cardInfo?.activation_status_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="设备型号">{{
cardInfo?.device_model || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="实名时间">{{
selectedDeviceStatusCard?.real_name_at
? formatDateTime(selectedDeviceStatusCard.real_name_at)
: '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="在线状态">
<ElTag
v-if="cardInfo?.online_status !== undefined"
:type="getOnlineStatusType(cardInfo?.online_status)"
size="small"
>
{{ getOnlineStatusName(cardInfo?.online_status) }}
</ElTag>
<span v-else>-</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="虚拟号">{{ cardInfo?.virtual_no || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">{{
getSwitchModeName(cardInfo?.switch_mode)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="已绑定卡数">{{
cardInfo?.bound_card_count || 0
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最大卡槽数">{{
cardInfo?.max_sim_slots || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem v-if="canShowSensitiveFields" label="实名认证策略">
{{ getRealnamePolicyName(cardInfo?.realname_policy) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="设备类型">{{ cardInfo?.device_type || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem v-if="canShowSensitiveFields" label="制造商">{{
cardInfo?.manufacturer || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="最后在线时间">{{
formatDateTime(cardInfo?.last_online_time) || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="SN">{{ cardInfo?.sn || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="所有套餐已用量">
{{ formatUsageSummaryMb(cardInfo?.total_virtual_used_mb) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="所有套餐剩余量">
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="最后同步时间">{{
formatDateTime(deviceRealtime?.last_sync_time) || '-'
}}</ElDescriptionsItem>
</template>
</ElDescriptions>
<ElDivider content-position="left">同步状态</ElDivider>
<ElDescriptions :column="descriptionsColumn" border>
<ElDescriptionsItem label="轮询状态">
<ElTag
v-if="cardInfo?.polling"
:type="cardInfo.polling.enabled ? 'success' : 'info'"
size="small"
>
{{ cardInfo.polling.enabled ? '已启用' : '未启用' }}
</ElTag>
<span v-else>-</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="最后活跃时间">
{{ formatDateTime(cardInfo?.polling?.last_activity_at) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="同步轨迹">
<ElButton
v-permission="'asset_info:view_sync_trail'"
type="primary"
link
@click="emit('viewSyncTrail')"
>
查看同步轨迹
</ElButton>
</ElDescriptionsItem>
</ElDescriptions>
<template v-if="previousExchangeAsset || nextExchangeAsset">
<ElDivider content-position="left">换货链路</ElDivider>
<ElDescriptions :column="descriptionsColumn" border>
<ElDescriptionsItem v-if="previousExchangeAsset" label="前代资产">
<div class="exchange-asset">
<ElTag type="success" size="small">换货新资产</ElTag>
<ElButton
v-if="previousExchangeAsset.can_view"
type="primary"
link
@click="handleNavigateToExchangeAsset(previousExchangeAsset)"
>
{{ previousExchangeAsset.identifier }}
</ElButton>
<span v-else>{{ previousExchangeAsset.identifier }}</span>
<span v-if="previousExchangeAsset.exchange_no" class="exchange-no">
换货单{{ previousExchangeAsset.exchange_no }}
</span>
</div>
</ElDescriptionsItem>
<ElDescriptionsItem v-if="nextExchangeAsset" label="后代资产">
<div class="exchange-asset">
<ElTag type="warning" size="small">已换出旧资产</ElTag>
<ElButton
v-if="nextExchangeAsset.can_view"
type="primary"
link
@click="handleNavigateToExchangeAsset(nextExchangeAsset)"
>
{{ nextExchangeAsset.identifier }}
</ElButton>
<span v-else>{{ nextExchangeAsset.identifier }}</span>
<span v-if="nextExchangeAsset.exchange_no" class="exchange-no">
换货单{{ nextExchangeAsset.exchange_no }}
</span>
</div>
</ElDescriptionsItem>
</ElDescriptions>
</template>
<!-- 设备绑定卡列表 -->
<template v-if="cardInfo?.asset_type === 'device'">
<ElDivider content-position="left">绑定卡列表</ElDivider>
<ElTable
v-if="cardInfo.cards && cardInfo.cards.length > 0"
:data="sortedDeviceCards"
border
style="width: 100%; margin-top: 16px"
>
<ElTableColumn label="ICCID" width="220" show-overflow-tooltip>
<template #default="scope">
<div style="display: flex; align-items: center; justify-content: space-between">
<ElButton type="primary" link @click="handleNavigateToCardInfo(scope.row.iccid)">
{{ scope.row.iccid }}
</ElButton>
<el-icon
class="copy-icon"
@click="handleCopyICCID(scope.row.iccid)"
style="color: var(--el-color-primary); cursor: pointer"
>
<CopyDocument />
</el-icon>
</div>
</template>
</ElTableColumn>
<ElTableColumn prop="msisdn" label="MSISDN" min-width="150" showOverflowTooltip />
<ElTableColumn prop="carrier_name" label="运营商" min-width="120" showOverflowTooltip />
<ElTableColumn prop="slot_position" label="卡槽位置" width="130">
<template #default="scope">
<div style="display: flex; gap: 10px">
<span>SIM-{{ scope.row.slot_position }}</span>
<ElTag v-if="scope.row.is_current" type="primary" size="small">当前</ElTag>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="网络状态" width="100">
<template #default="scope">
<ElTag :type="scope.row.network_status === 1 ? 'success' : 'danger'" size="small">
{{ scope.row.network_status === 1 ? '正常' : '停机' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="实名状态" width="100">
<template #default="scope">
<ElTag :type="getRealNameStatusType(scope.row.real_name_status)" size="small">
{{ getRealNameStatusName(scope.row.real_name_status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="实名认证策略" width="120" showOverflowTooltip>
<template #default="scope">
{{ getRealnamePolicyName(scope.row.realname_policy) }}
</template>
</ElTableColumn>
<ElTableColumn label="实名时间" width="170">
<template #default="scope">
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="运营商停机原因" min-width="220" showOverflowTooltip>
<template #default="scope">
{{ scope.row.gateway_extend || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="180" fixed="right">
<template #default="scope">
<!-- 根据网络状态显示启用或停用按钮 -->
<ElButton
v-if="scope.row.network_status === 0"
type="success"
size="small"
link
:disabled="isBindingStartDisabled(scope.row)"
@click="handleEnableBindingCard(scope.row)"
v-permission="'iot_info:start'"
>
{{ getBindingStartButtonText(scope.row) }}
</ElButton>
<ElButton
v-if="scope.row.network_status === 1"
type="warning"
size="small"
link
:disabled="isBindingStopDisabled(scope.row)"
@click="handleDisableBindingCard(scope.row)"
v-permission="'iot_info:stop'"
>
{{ getBindingStopButtonText(scope.row) }}
</ElButton>
<ElButton
type="primary"
size="small"
link
@click="handleUpdateBindingCardRealnameStatus(scope.row)"
v-permission="'bound_card:update_realname_status'"
>
更新实名状态
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<ElEmpty v-else description="暂无绑定卡" :image-size="80" style="margin-top: 16px" />
<!-- 设备实时信息 -->
<ElDivider content-position="left">设备实时信息</ElDivider>
<div v-if="hasDeviceRealtimeSection" v-loading="realtimeLoading" style="margin-top: 16px">
<ElDescriptions :column="descriptionsColumn" border>
<!-- 基本状态 -->
<ElDescriptionsItem label="在线状态">
<ElTag :type="getOnlineStatusType(deviceRealtime?.online_status)" size="small">
{{ getOnlineStatusName(deviceRealtime?.online_status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="电量">
{{
deviceRealtime?.battery_level !== null && deviceRealtime?.battery_level !== undefined
? `${deviceRealtime?.battery_level}%`
: '-'
}}
</ElDescriptionsItem>
<!-- 时间信息 -->
<ElDescriptionsItem label="本次开机时长">
{{ formatDuration(deviceRealtime?.run_time) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="本次联网时长">
{{ formatDuration(deviceRealtime?.connect_time) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="最后在线时间">
{{ formatDateTime(deviceRealtime?.last_online_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="实名同步时间">
{{ formatDateTime(cardInfo?.last_real_name_check_at) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="卡状态同步时间">
{{ formatDateTime(cardInfo?.last_card_status_check_at) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="流量同步时间">
{{ formatDateTime(cardInfo?.last_data_check_at) || '-' }}
</ElDescriptionsItem>
<!-- 信号信息 -->
<ElDescriptionsItem label="信号综合判断">
<ElTag :type="getSignalQualityType(deviceRealtime?.signal_quality)" size="small">
{{ getSignalQualityText(deviceRealtime?.signal_quality) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="信号较弱原因" :span="3">
{{ getSignalBadReasonText(deviceRealtime?.signal_bad_reason) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="已连接设备数">
{{ deviceRealtime?.client_number ?? '-' }} / {{ deviceRealtime?.max_clients ?? '-' }}
</ElDescriptionsItem>
<!-- WiFi 信息 -->
<ElDescriptionsItem label="WiFi 状态">
<ElTag :type="deviceRealtime?.wifi_enabled ? 'success' : 'info'" size="small">
{{
deviceRealtime?.wifi_enabled === undefined
? '-'
: deviceRealtime.wifi_enabled
? '已开启'
: '已关闭'
}}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="WiFi 名称">
{{ deviceRealtime?.ssid || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="WiFi 密码">
{{ deviceRealtime?.wifi_password || '-' }}
</ElDescriptionsItem>
<!-- 网络信息 -->
<ElDescriptionsItem label="WAN IP">
{{ deviceRealtime?.wan_ip || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="LAN IP">
{{ deviceRealtime?.lan_ip || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="MAC 地址">
{{ deviceRealtime?.mac_address || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="IMSI">
{{ deviceRealtime?.imsi || '-' }}
</ElDescriptionsItem>
<!-- 设备配置 -->
<ElDescriptionsItem label="限速">
{{
deviceRealtime?.limit_speed === undefined
? '-'
: deviceRealtime.limit_speed
? `${deviceRealtime.limit_speed} KB/s`
: '不限速'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="固件版本">
{{ deviceRealtime?.software_version || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="切卡模式">
{{
deviceRealtime?.switch_mode === '0'
? '自动'
: deviceRealtime?.switch_mode === '1'
? '手动'
: '-'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="上报周期">
{{ deviceRealtime?.sync_interval ? `${deviceRealtime.sync_interval}` : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="信息更新时间">
{{ formatDateTime(deviceRealtime?.last_update_time) || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
</div>
<ElEmpty v-else description="暂无实时信息" :image-size="80" style="margin-top: 16px" />
</template>
<!-- IoT卡操作按钮 -->
<ElAlert
v-if="startErrorMessage"
:title="startErrorMessage"
type="error"
:closable="false"
show-icon
style="margin-top: 16px"
/>
<div
v-if="cardInfo?.asset_type === 'card'"
class="card-operations"
style="margin-top: 16px; text-align: right"
>
<ElButton
v-if="cardInfo?.network_status === 0"
v-permission="'iot_info:start'"
type="success"
:disabled="startDisabled"
@click="handleEnableCard"
>
{{ startRemainingText ? `启用此卡(${startRemainingText})` : '启用此卡' }}
</ElButton>
<ElButton
v-if="cardInfo?.network_status === 1"
v-permission="'iot_info:stop'"
type="warning"
:disabled="stopDisabled"
@click="handleDisableCard"
>
{{ stopRemainingText ? `停用此卡(${stopRemainingText})` : '停用此卡' }}
</ElButton>
<!--<ElButton type="danger" v-permission="'iot_info:handler_stop'" @click="handleManualDeactivate"> 手动停用 </ElButton>-->
<!-- 手动停用暂时不开放 -->
<ElButton
v-if="canShowRealnamePolicy"
v-permission="'asset:card_realname_policy'"
type="primary"
@click="handleShowRealnamePolicy"
>
实名认证策略
</ElButton>
<ElButton
v-permission="'iot_info:update_realname_status'"
type="primary"
@click="handleShowUpdateRealnameStatus"
>
更新实名状态
</ElButton>
</div>
<!-- 设备操作按钮 -->
<div v-else class="device-operations" style="margin-top: 16px; text-align: right">
<ElButton v-permission="'device_info:switch_mode'" type="info" @click="handleShowSwitchMode"
>设置切卡模式</ElButton
>
<ElButton v-permission="'device_info:reboot'" type="primary" @click="handleRebootDevice">
重启设备
</ElButton>
<ElButton v-permission="'device_info:reset'" type="warning" @click="handleResetDevice">
恢复出厂
</ElButton>
<ElButton
v-permission="'device_info:switch_card'"
type="success"
@click="handleShowSwitchCard"
>
切换SIM卡
</ElButton>
<ElButton v-permission="'device_info:set_wifi'" type="primary" @click="handleShowSetWiFi">
设置WiFi
</ElButton>
<ElButton
v-if="canShowRealnamePolicy"
v-permission="'asset_info:device_realname_policy'"
type="primary"
@click="handleShowRealnamePolicy"
>
实名认证策略
</ElButton>
</div>
</ElCard>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import {
ElCard,
ElDescriptions,
ElDescriptionsItem,
ElTag,
ElTooltip,
ElSwitch,
ElDivider,
ElTable,
ElTableColumn,
ElButton,
ElAlert,
ElEmpty,
ElMessageBox,
ElMessage
} from 'element-plus'
import { CopyDocument } from '@element-plus/icons-vue'
import type { AssetExchangeTrace, AssetExchangeTraceAsset, AssetPollingStatus } from '@/types/api'
import { useAssetFormatters } from '../composables/useAssetFormatters'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
// Props
interface BindingCard {
id: number
iccid: string
msisdn?: string
carrier_name?: string
slot_position?: number
is_current: boolean
network_status?: number
real_name_status?: number
real_name_at?: string | null
realname_policy?: string
gateway_extend?: string
last_data_check_at?: string | null
last_real_name_check_at?: string | null
last_card_status_check_at?: string | null
}
interface AssetInfo {
asset_type: 'card' | 'device'
iccid?: string
imei?: string
msisdn?: string
carrier_type?: string
carrier_name?: string
real_name_status?: number
activation_status_name?: string
realname_policy?: string
network_status?: number
current_month_usage_mb?: number
last_gateway_reading_mb?: number
device_type?: string
device_name?: string
device_model?: string
virtual_no?: string
sn?: string
cards?: BindingCard[]
status?: number
shop_name?: string
series_name?: string
real_name_at?: string | null
supplier?: string
gateway_card_imei?: string
gateway_extend?: string
total_virtual_used_mb?: number | null
total_virtual_remaining_mb?: number | null
last_sync_time?: string | null
last_data_check_at?: string | null
last_real_name_check_at?: string | null
last_card_status_check_at?: string | null
polling?: AssetPollingStatus | null
exchange_trace?: AssetExchangeTrace | null
bound_device_id?: number
bound_device_no?: string
bound_device_name?: string
bound_card_count?: number
max_sim_slots?: number
activated_at?: string
online_status?: number
software_version?: string
switch_mode?: string
last_online_time?: string
manufacturer?: string
}
interface DeviceRealtimeInfo {
online_status?: number
status?: number
battery_level?: number | null
run_time?: string
connect_time?: string
last_online_time?: string | null
client_number?: number
max_clients?: number
current_iccid?: string
signal_quality?: string
signal_bad_reason?: string
wifi_enabled?: boolean | number
ssid?: string
wifi_password?: string
ip_address?: string
wan_ip?: string
lan_ip?: string
mac_address?: string
imei?: string
imsi?: string
limit_speed?: number
software_version?: string
switch_mode?: string
sync_interval?: number
device_id?: string
last_sync_time?: string | null
last_update_time?: string | null
}
interface Props {
cardInfo: AssetInfo
deviceRealtime: DeviceRealtimeInfo | null
pollingEnabled: boolean
realtimeLoading?: boolean
startDisabled?: boolean
startErrorMessage?: string
stopDisabled?: boolean
startRemainingText?: string
stopRemainingText?: string
getStartRemainingText?: (identifier: string) => string
getStopRemainingText?: (identifier: string) => string
isStartDisabled?: (identifier: string) => boolean
isStopDisabled?: (identifier: string) => boolean
}
const props = withDefaults(defineProps<Props>(), {
realtimeLoading: false,
startDisabled: false,
startErrorMessage: '',
stopDisabled: false,
startRemainingText: '',
stopRemainingText: ''
})
const { hasAuth } = useAuth()
const userStore = useUserStore()
const { width } = useWindowSize()
const CARD_MONTH_USAGE_PERMISSION = 'asset_info:view_card_month_usage'
const realnamePolicyPermission = computed(() => {
return props.cardInfo?.asset_type === 'card'
? 'asset:card_realname_policy'
: 'asset:device_realname_policy'
})
const canShowRealnamePolicy = computed(() => hasAuth(realnamePolicyPermission.value))
const pollingPermission = computed(() => {
return props.cardInfo?.asset_type === 'card'
? 'iot_info:auto_polling'
: 'device_info:auto_polling'
})
const canShowPolling = computed(() => hasAuth(pollingPermission.value))
const canShowCardMonthUsage = computed(() => {
const userType = Number(userStore.info.user_type ?? 0)
return ![3, 4].includes(userType) && hasAuth(CARD_MONTH_USAGE_PERMISSION)
})
const isAgentOrEnterpriseAccount = computed(() => {
const userType = Number(userStore.info.user_type ?? 0)
return [3, 4].includes(userType)
})
const canShowSensitiveFields = computed(() => {
return !isAgentOrEnterpriseAccount.value
})
const carrierAccountDisplay = computed(() => {
if (isAgentOrEnterpriseAccount.value) {
return getCarrierTypeName(props.cardInfo?.carrier_type)
}
return props.cardInfo?.carrier_name || '-'
})
const descriptionsColumn = computed(() => {
if (width.value <= 640) return 1
if (width.value <= 1024) return 2
return 4
})
// Emits
interface Emits {
(e: 'update:pollingEnabled', value: boolean): void
(e: 'enableCard'): void
(e: 'disableCard'): void
(e: 'manualDeactivate'): void
(e: 'rebootDevice'): void
(e: 'resetDevice'): void
(e: 'showSpeedLimit'): void
(e: 'showSwitchCard'): void
(e: 'showSwitchMode'): void
(e: 'show-set-wifi'): void
(e: 'showRealnamePolicy'): void
(e: 'showUpdateRealnameStatus'): void
(e: 'enableBindingCard', payload: { card: BindingCard }): void
(e: 'disableBindingCard', payload: { card: BindingCard }): void
(e: 'updateBindingCardRealnameStatus', payload: { card: BindingCard }): void
(e: 'navigateToCard', iccid: string): void
(e: 'navigateToDevice', deviceNo: string): void
(e: 'viewSyncTrail'): void
}
const emit = defineEmits<Emits>()
// 使用格式化工具
const {
formatDataSize,
formatDuration,
getRealNameStatusName,
getRealNameStatusType,
getRealnamePolicyName,
getCarrierTypeName,
getAssetStatusName,
getAssetStatusType,
getOnlineStatusName,
getOnlineStatusType,
getSignalQualityText,
getSignalQualityType,
getSignalBadReasonText,
getSwitchModeName
} = useAssetFormatters()
// 排序后的设备卡列表(按卡槽位置排序,当前卡排在最前面)
const sortedDeviceCards = computed(() => {
if (!props.cardInfo?.cards || props.cardInfo.cards.length === 0) {
return []
}
const cards = [...props.cardInfo.cards]
return cards.sort((a, b) => {
if (a.is_current && !b.is_current) return -1
if (!a.is_current && b.is_current) return 1
return (a.slot_position ?? 0) - (b.slot_position ?? 0)
})
})
const previousExchangeAsset = computed(
() => props.cardInfo.exchange_trace?.previous_asset ?? null
)
const nextExchangeAsset = computed(() => props.cardInfo.exchange_trace?.next_asset ?? null)
// 设备详情里的实名状态和实名时间从选中的绑定卡取值:
// 只有一张卡时直接取该卡,多张卡时仅取 is_current=true 的卡。
const selectedDeviceStatusCard = computed(() => {
if (!props.cardInfo?.cards || props.cardInfo.cards.length === 0) {
return null
}
if (props.cardInfo.cards.length === 1) {
return props.cardInfo.cards[0]
}
return props.cardInfo.cards.find((card: BindingCard) => card.is_current) || null
})
const hasDeviceRealtimeSection = computed(
() =>
Boolean(props.deviceRealtime) ||
props.cardInfo?.last_data_check_at !== undefined ||
props.cardInfo?.last_real_name_check_at !== undefined ||
props.cardInfo?.last_card_status_check_at !== undefined
)
const formatUsageSummaryMb = (value?: number | null) => {
if (value === undefined || value === null) return '-'
return formatDataSize(value)
}
// 处理轮询开关变化
const handlePollingChange = (value: string | number | boolean) => {
emit('update:pollingEnabled', Boolean(value))
}
// 跳转到卡片信息
const handleNavigateToCardInfo = (iccid: string) => {
// 触发搜索事件,让父组件处理
emit('navigateToCard', iccid)
}
const handleNavigateToExchangeAsset = (asset: AssetExchangeTraceAsset) => {
if (!asset.can_view) return
if (asset.asset_type === 'card') {
emit('navigateToCard', asset.identifier)
return
}
emit('navigateToDevice', asset.identifier)
}
// 复制ICCID
const handleCopyICCID = async (iccid: string) => {
try {
await navigator.clipboard.writeText(iccid)
ElMessage.success('ICCID已复制')
} catch {
ElMessage.error('复制失败')
}
}
// 卡操作
const handleEnableCard = () => {
if (props.startDisabled) return
emit('enableCard')
}
const handleDisableCard = () => {
if (props.stopDisabled) return
emit('disableCard')
}
// 设备操作
const handleRebootDevice = () => {
emit('rebootDevice')
}
const handleResetDevice = () => {
emit('resetDevice')
}
const handleShowSwitchCard = () => {
emit('showSwitchCard')
}
const handleShowSwitchMode = () => {
emit('showSwitchMode')
}
const handleShowSetWiFi = () => {
emit('show-set-wifi')
}
const handleShowRealnamePolicy = () => {
emit('showRealnamePolicy')
}
const handleShowUpdateRealnameStatus = () => {
emit('showUpdateRealnameStatus')
}
// 绑定卡操作
const getBindingStartRemainingText = (card: BindingCard) =>
props.getStartRemainingText?.(card.iccid) || ''
const getBindingStopRemainingText = (card: BindingCard) =>
props.getStopRemainingText?.(card.iccid) || ''
const isBindingStartDisabled = (card: BindingCard) => props.isStartDisabled?.(card.iccid) || false
const isBindingStopDisabled = (card: BindingCard) => props.isStopDisabled?.(card.iccid) || false
const getBindingStartButtonText = (card: BindingCard) => {
const remainingText = getBindingStartRemainingText(card)
return remainingText ? `启用此卡(${remainingText})` : '启用此卡'
}
const getBindingStopButtonText = (card: BindingCard) => {
const remainingText = getBindingStopRemainingText(card)
return remainingText ? `停用此卡(${remainingText})` : '停用此卡'
}
const handleEnableBindingCard = (card: BindingCard) => {
if (isBindingStartDisabled(card)) return
ElMessageBox.confirm(`确定要启用此卡(${card.iccid})吗?`, '启用确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
emit('enableBindingCard', { card })
})
.catch(() => {})
}
const handleDisableBindingCard = (card: BindingCard) => {
if (isBindingStopDisabled(card)) return
ElMessageBox.confirm(`确定要停用此卡(${card.iccid})吗?`, '停用确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
emit('disableBindingCard', { card })
})
.catch(() => {})
}
const handleUpdateBindingCardRealnameStatus = (card: BindingCard) => {
emit('updateBindingCardRealnameStatus', { card })
}
</script>
<style scoped lang="scss">
.info-card {
&.basic-info {
.gateway-extend-text {
word-break: break-all;
}
.card-header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
.card-header-left {
display: flex;
gap: 8px;
align-items: center;
}
.card-header-right {
.polling-switch-wrapper {
display: flex;
gap: 8px;
align-items: center;
.polling-label {
font-size: 14px;
color: var(--el-text-color-regular);
}
}
}
}
:deep(.el-descriptions__body) {
overflow-x: auto;
}
:deep(.el-descriptions__cell) {
word-break: break-word;
}
:deep(.el-table) {
min-width: 760px;
}
.card-operations,
.device-operations {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
.el-button {
margin-left: 0;
}
}
.exchange-asset {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
.exchange-no {
color: var(--el-text-color-secondary);
}
}
@media (width <= 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
.card-header-left,
.card-header-right {
width: 100%;
}
.card-header-right {
display: flex;
justify-content: flex-start;
}
}
.card-operations,
.device-operations {
justify-content: stretch;
.el-button {
flex: 1 1 120px;
}
}
}
}
}
</style>