1057 lines
36 KiB
Vue
1057 lines
36 KiB
Vue
<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>
|