1794 lines
55 KiB
Vue
1794 lines
55 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">
|
|
<ElButton v-if="auditTarget" type="primary" link @click="openAssetAudit">
|
|
{{ isSubjectActivityView ? '活动记录' : '审计记录' }}
|
|
</ElButton>
|
|
<ElButton
|
|
v-if="
|
|
cardInfo?.asset_type === 'card' &&
|
|
hasAuth(JULY_PERMISSIONS.speedTier.view) &&
|
|
hasAuth(JULY_PERMISSIONS.speedTier.update)
|
|
"
|
|
type="primary"
|
|
link
|
|
@click="emit('showSpeedLimit')"
|
|
>
|
|
设置限速
|
|
</ElButton>
|
|
<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
|
|
class="asset-descriptions"
|
|
:column="descriptionsColumn"
|
|
direction="vertical"
|
|
border
|
|
size="large"
|
|
>
|
|
<!-- 卡专属字段 -->
|
|
<template v-if="cardInfo?.asset_type === 'card'">
|
|
<ElDescriptionsItem
|
|
label="ICCID"
|
|
label-class-name="asset-label asset-label--identifier-iccid"
|
|
>{{ cardInfo?.iccid || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="MSISDN"
|
|
label-class-name="asset-label asset-label--identifier-msisdn"
|
|
>{{ cardInfo?.msisdn || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="运营商账户"
|
|
label-class-name="asset-label asset-label--operator-account"
|
|
>{{ carrierAccountDisplay }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="实名状态"
|
|
label-class-name="asset-label asset-label--realname-status"
|
|
>
|
|
<ElTag :type="getRealNameStatusType(cardInfo?.real_name_status)" size="small">
|
|
{{ getRealNameStatusName(cardInfo?.real_name_status) }}
|
|
</ElTag>
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
v-if="canShowSensitiveFields"
|
|
label="实名认证策略"
|
|
label-class-name="asset-label asset-label--realname-policy"
|
|
>
|
|
{{ getRealnamePolicyName(cardInfo?.realname_policy) }}
|
|
</ElDescriptionsItem>
|
|
|
|
<ElDescriptionsItem label="店铺" label-class-name="asset-label asset-label--shop">{{
|
|
cardInfo?.shop_name || '-'
|
|
}}</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="套餐系列"
|
|
label-class-name="asset-label asset-label--package-series"
|
|
>{{ cardInfo?.series_name || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="虚拟号"
|
|
label-class-name="asset-label asset-label--virtual-number"
|
|
>{{ cardInfo?.virtual_no || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="网络状态"
|
|
label-class-name="asset-label asset-label--network-status"
|
|
>
|
|
<ElTag :type="cardInfo?.network_status === 1 ? 'success' : 'danger'" size="small">
|
|
{{ cardInfo?.network_status === 1 ? '正常' : '停机' }}
|
|
</ElTag>
|
|
</ElDescriptionsItem>
|
|
|
|
<ElDescriptionsItem
|
|
v-if="canShowCardMonthUsage"
|
|
label="本月已用流量"
|
|
label-class-name="asset-label asset-label--usage-month"
|
|
>{{ formatDataSize(cardInfo?.current_month_usage_mb || 0) }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
v-if="canShowCardMonthUsage"
|
|
label="运营商周期月内已用流量"
|
|
label-class-name="asset-label asset-label--usage-cycle"
|
|
>{{ formatDataSize(cardInfo?.last_gateway_reading_mb || 0) }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="实名时间"
|
|
label-class-name="asset-label asset-label--realname-time"
|
|
>{{
|
|
cardInfo?.real_name_at ? formatAssetInfoTime(cardInfo.real_name_at) : '-'
|
|
}}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="实名同步时间"
|
|
label-class-name="asset-label asset-label--realname-sync"
|
|
>{{ formatAssetInfoTime(cardInfo?.last_real_name_check_at) }}</ElDescriptionsItem
|
|
>
|
|
|
|
<ElDescriptionsItem
|
|
label="IMEI"
|
|
label-class-name="asset-label asset-label--identifier-imei"
|
|
>{{ cardInfo?.gateway_card_imei || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="资产状态"
|
|
label-class-name="asset-label asset-label--asset-status"
|
|
>
|
|
<ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">
|
|
{{ getAssetStatusName(cardInfo?.status) }}
|
|
</ElTag>
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="卡状态同步时间"
|
|
label-class-name="asset-label asset-label--card-sync"
|
|
>{{ formatAssetInfoTime(cardInfo?.last_card_status_check_at) }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="流量同步时间"
|
|
label-class-name="asset-label asset-label--traffic-sync"
|
|
>{{ formatAssetInfoTime(cardInfo?.last_data_check_at) }}</ElDescriptionsItem
|
|
>
|
|
|
|
<ElDescriptionsItem
|
|
label="运营商状态"
|
|
label-class-name="asset-label asset-label--operator-status"
|
|
:span="1"
|
|
>
|
|
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="所有套餐已用量"
|
|
label-class-name="asset-label asset-label--package-used"
|
|
>
|
|
{{ formatUsageSummaryMb(cardInfo?.total_virtual_used_mb) }}
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="所有套餐剩余量"
|
|
label-class-name="asset-label asset-label--package-remaining"
|
|
>
|
|
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="预计套餐到期时间"
|
|
label-class-name="asset-label asset-label--package-expire-estimate"
|
|
>
|
|
<span
|
|
:class="getExpiryEstimateClass(cardInfo)"
|
|
:title="getExpiryEstimateTooltip(cardInfo)"
|
|
>
|
|
{{ getExpiryEstimateText(cardInfo) }}
|
|
</span>
|
|
</ElDescriptionsItem>
|
|
|
|
<template v-if="cardInfo?.bound_device_id">
|
|
<ElDescriptionsItem
|
|
label="绑定设备号"
|
|
label-class-name="asset-label asset-label--bound-device-number"
|
|
>
|
|
<ElButton
|
|
type="primary"
|
|
link
|
|
@click="emit('navigateToDevice', cardInfo!.bound_device_no!)"
|
|
>
|
|
{{ cardInfo?.bound_device_no || '-' }}
|
|
</ElButton>
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="绑定设备名称"
|
|
label-class-name="asset-label asset-label--bound-device-name"
|
|
>
|
|
{{ cardInfo?.bound_device_name || '-' }}
|
|
</ElDescriptionsItem>
|
|
</template>
|
|
</template>
|
|
|
|
<!-- 设备专属字段 -->
|
|
<template v-if="cardInfo?.asset_type === 'device'">
|
|
<ElDescriptionsItem
|
|
label="IMEI"
|
|
label-class-name="asset-label asset-label--identifier-imei"
|
|
>{{ cardInfo?.imei || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="设备名称"
|
|
label-class-name="asset-label asset-label--device-name"
|
|
>{{ cardInfo?.device_name || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem label="店铺" label-class-name="asset-label asset-label--shop">{{
|
|
cardInfo?.shop_name || '-'
|
|
}}</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="设备状态"
|
|
label-class-name="asset-label asset-label--device-status"
|
|
>
|
|
<ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">
|
|
{{ getAssetStatusName(cardInfo?.status) }}
|
|
</ElTag>
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="实名状态"
|
|
label-class-name="asset-label asset-label--realname-status"
|
|
>
|
|
<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="虚拟号"
|
|
label-class-name="asset-label asset-label--virtual-number"
|
|
>{{ cardInfo?.virtual_no || '-' }}</ElDescriptionsItem
|
|
>
|
|
|
|
<ElDescriptionsItem
|
|
label="设备型号"
|
|
label-class-name="asset-label asset-label--device-model"
|
|
>{{ cardInfo?.device_model || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="实名时间"
|
|
label-class-name="asset-label asset-label--realname-time"
|
|
>{{
|
|
selectedDeviceStatusCard?.real_name_at
|
|
? formatAssetInfoTime(selectedDeviceStatusCard.real_name_at)
|
|
: '-'
|
|
}}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="在线状态"
|
|
label-class-name="asset-label asset-label--online-status"
|
|
>
|
|
<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="激活状态"
|
|
label-class-name="asset-label asset-label--activation-status"
|
|
>
|
|
{{ cardInfo?.activation_status_name || '-' }}
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="切卡模式"
|
|
label-class-name="asset-label asset-label--switch-mode"
|
|
>{{ getSwitchModeName(cardInfo?.switch_mode) }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="已绑定卡数"
|
|
label-class-name="asset-label asset-label--bound-card-count"
|
|
>{{ cardInfo?.bound_card_count || 0 }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="最大卡槽数"
|
|
label-class-name="asset-label asset-label--max-slots"
|
|
>{{ cardInfo?.max_sim_slots || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
v-if="canShowSensitiveFields"
|
|
label="实名认证策略"
|
|
label-class-name="asset-label asset-label--realname-policy"
|
|
>
|
|
{{ getRealnamePolicyName(cardInfo?.realname_policy) }}
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="设备类型"
|
|
label-class-name="asset-label asset-label--device-type"
|
|
>{{ cardInfo?.device_type || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
v-if="canShowSensitiveFields"
|
|
label="制造商"
|
|
label-class-name="asset-label asset-label--manufacturer"
|
|
>{{ cardInfo?.manufacturer || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem
|
|
label="最后在线时间"
|
|
label-class-name="asset-label asset-label--last-online"
|
|
>{{ formatDateTime(cardInfo?.last_online_time) || '-' }}</ElDescriptionsItem
|
|
>
|
|
<ElDescriptionsItem label="SN" label-class-name="asset-label asset-label--identifier-sn">{{
|
|
cardInfo?.sn || '-'
|
|
}}</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="所有套餐已用量"
|
|
label-class-name="asset-label asset-label--package-used"
|
|
>
|
|
{{ formatUsageSummaryMb(cardInfo?.total_virtual_used_mb) }}
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="所有套餐剩余量"
|
|
label-class-name="asset-label asset-label--package-remaining"
|
|
>
|
|
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="预计套餐到期时间"
|
|
label-class-name="asset-label asset-label--package-expire-estimate"
|
|
>
|
|
<span
|
|
:class="getExpiryEstimateClass(cardInfo)"
|
|
:title="getExpiryEstimateTooltip(cardInfo)"
|
|
>
|
|
{{ getExpiryEstimateText(cardInfo) }}
|
|
</span>
|
|
</ElDescriptionsItem>
|
|
<ElDescriptionsItem
|
|
label="最后同步时间"
|
|
label-class-name="asset-label asset-label--last-sync"
|
|
>{{ formatDateTime(deviceRealtime?.last_sync_time) || '-' }}</ElDescriptionsItem
|
|
>
|
|
</template>
|
|
</ElDescriptions>
|
|
|
|
<template v-if="previousExchangeAsset || nextExchangeAsset">
|
|
<ElDivider content-position="left">换货链路</ElDivider>
|
|
<ElDescriptions :column="descriptionsColumn" border size="large">
|
|
<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 class="info-section-divider" content-position="left">
|
|
<span class="section-divider-title">绑定卡列表</span>
|
|
</ElDivider>
|
|
<div
|
|
v-if="cardInfo.cards && cardInfo.cards.length > 0"
|
|
class="binding-card-list"
|
|
:class="`binding-card-list--${Math.min(sortedDeviceCards.length, 3)}`"
|
|
>
|
|
<ElCard
|
|
v-for="bindingCard in sortedDeviceCards"
|
|
:key="bindingCard.iccid"
|
|
class="binding-card"
|
|
shadow="never"
|
|
:class="{ 'binding-card--current': bindingCard.is_current }"
|
|
>
|
|
<div class="binding-card-header">
|
|
<div class="binding-card-identifier">
|
|
<ElButton type="primary" link @click="handleNavigateToCardInfo(bindingCard.iccid)">
|
|
{{ bindingCard.iccid }}
|
|
</ElButton>
|
|
<el-icon class="copy-icon" @click="handleCopyICCID(bindingCard.iccid)" title="复制">
|
|
<CopyDocument />
|
|
</el-icon>
|
|
</div>
|
|
<ElTag
|
|
v-if="bindingCard.is_current"
|
|
class="binding-card-current-tag"
|
|
type="primary"
|
|
size="default"
|
|
>
|
|
当前
|
|
</ElTag>
|
|
</div>
|
|
|
|
<div class="binding-card-details">
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">MSISDN</span>
|
|
<span class="binding-card-field-value">{{ bindingCard.msisdn || '-' }}</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">运营商</span>
|
|
<span class="binding-card-field-value">{{ getCarrierAccountDisplay(bindingCard) }}</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">卡槽位置</span>
|
|
<span class="binding-card-field-value">
|
|
SIM-{{ bindingCard.slot_position ?? '-' }}
|
|
</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">网络状态</span>
|
|
<span class="binding-card-field-value">
|
|
<ElTag :type="bindingCard.network_status === 1 ? 'success' : 'danger'" size="small">
|
|
{{ bindingCard.network_status === 1 ? '正常' : '停机' }}
|
|
</ElTag>
|
|
</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">实名状态</span>
|
|
<span class="binding-card-field-value">
|
|
<ElTag :type="getRealNameStatusType(bindingCard.real_name_status)" size="small">
|
|
{{ getRealNameStatusName(bindingCard.real_name_status) }}
|
|
</ElTag>
|
|
</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">实名认证策略</span>
|
|
<span class="binding-card-field-value">
|
|
{{ getRealnamePolicyName(bindingCard.realname_policy) }}
|
|
</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">实名时间</span>
|
|
<span class="binding-card-field-value">
|
|
{{
|
|
bindingCard.real_name_at
|
|
? formatAssetInfoTime(bindingCard.real_name_at)
|
|
: '-'
|
|
}}
|
|
</span>
|
|
</div>
|
|
<div class="binding-card-field">
|
|
<span class="binding-card-field-label">运营商停机原因</span>
|
|
<span class="binding-card-field-value">{{ bindingCard.gateway_extend || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="binding-card-actions">
|
|
<ElButton
|
|
v-if="bindingCard.network_status === 0"
|
|
type="success"
|
|
size="default"
|
|
:disabled="isBindingStartDisabled(bindingCard)"
|
|
@click="handleEnableBindingCard(bindingCard)"
|
|
v-permission="'iot_info:start'"
|
|
>
|
|
{{ getBindingStartButtonText(bindingCard) }}
|
|
</ElButton>
|
|
<ElButton
|
|
v-if="bindingCard.network_status === 1"
|
|
type="warning"
|
|
size="default"
|
|
:disabled="isBindingStopDisabled(bindingCard)"
|
|
@click="handleDisableBindingCard(bindingCard)"
|
|
v-permission="'iot_info:stop'"
|
|
>
|
|
{{ getBindingStopButtonText(bindingCard) }}
|
|
</ElButton>
|
|
<ElButton
|
|
type="primary"
|
|
size="default"
|
|
@click="handleUpdateBindingCardRealnameStatus(bindingCard)"
|
|
v-permission="'bound_card:update_realname_status'"
|
|
>
|
|
更新实名状态
|
|
</ElButton>
|
|
<ElButton
|
|
v-if="getBindingCardAuditTarget(bindingCard)"
|
|
type="primary"
|
|
size="default"
|
|
@click="openBindingCardAudit(bindingCard)"
|
|
>
|
|
卡审计
|
|
</ElButton>
|
|
</div>
|
|
</ElCard>
|
|
</div>
|
|
<ElEmpty v-else description="暂无绑定卡" :image-size="80" style="margin-top: 16px" />
|
|
|
|
<slot name="current-package" />
|
|
|
|
<!-- 设备实时信息 -->
|
|
<ElDivider class="info-section-divider" content-position="left">
|
|
<span class="section-divider-title">设备实时信息</span>
|
|
</ElDivider>
|
|
<div v-if="hasDeviceRealtimeSection" v-loading="realtimeLoading" class="realtime-section">
|
|
<ElDescriptions
|
|
class="asset-descriptions"
|
|
:column="descriptionsColumn"
|
|
direction="vertical"
|
|
border
|
|
size="large"
|
|
>
|
|
<ElDescriptionsItem
|
|
v-for="item in realtimeDescriptionItems"
|
|
:key="item.key"
|
|
:label="item.label"
|
|
:label-class-name="`asset-label asset-label--${item.icon}`"
|
|
:span="item.span ?? 1"
|
|
>
|
|
<ElTag v-if="item.tagType" :type="item.tagType" size="small">
|
|
{{ item.value }}
|
|
</ElTag>
|
|
<span v-else>{{ item.value }}</span>
|
|
</ElDescriptionsItem>
|
|
</ElDescriptions>
|
|
</div>
|
|
<ElEmpty v-else description="暂无实时信息" :image-size="80" style="margin-top: 16px" />
|
|
</template>
|
|
|
|
<slot v-if="cardInfo?.asset_type !== 'device'" name="current-package" />
|
|
|
|
<!-- 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>-->
|
|
<!-- 手动停用暂时不开放 -->
|
|
<!--suppress VueUnrecognizedDirective -->
|
|
<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,
|
|
ElButton,
|
|
ElAlert,
|
|
ElEmpty,
|
|
ElMessageBox,
|
|
ElMessage
|
|
} from 'element-plus'
|
|
import { CopyDocument } from '@element-plus/icons-vue'
|
|
import type {
|
|
AssetExchangeTrace,
|
|
AssetExchangeTraceAsset,
|
|
AssetPollingStatus,
|
|
ExpiryEstimateStatus
|
|
} from '@/types/api'
|
|
import { useAssetFormatters } from '../composables/useAssetFormatters'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
import { JULY_PERMISSIONS } from '@/config/constants'
|
|
import { useUserStore } from '@/store/modules/user'
|
|
import { formatDateTime } from '@/utils/business/format'
|
|
import {
|
|
getExpiryEstimateText,
|
|
getExpiryEstimateTooltip,
|
|
getExpiryEstimateClass
|
|
} from '@/utils/business/expiryEstimate'
|
|
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
|
|
import { resolveAuditResourceTarget } from '@/utils/business/auditNavigation'
|
|
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
|
|
|
|
// Props
|
|
interface BindingCard {
|
|
card_id?: number
|
|
iccid: string
|
|
msisdn?: string
|
|
carrier_type?: 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_id: number
|
|
asset_type: 'card' | 'device'
|
|
identifier: string
|
|
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
|
|
estimated_final_expires_at?: string | null
|
|
days_until_final_expiry?: number | null
|
|
expiry_estimate_status?: ExpiryEstimateStatus
|
|
expiry_estimate_status_name?: string | null
|
|
is_expiring?: boolean
|
|
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 isSubjectActivityView = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
|
|
const auditTarget = computed(() => {
|
|
const userType = Number(userStore.info.user_type)
|
|
const isCard = props.cardInfo.asset_type === 'card'
|
|
const permission =
|
|
userType === 3
|
|
? isCard
|
|
? AUDIT_PERMISSIONS.agentAssetInfoCardActivity
|
|
: AUDIT_PERMISSIONS.agentAssetInfoDeviceActivity
|
|
: userType === 4
|
|
? isCard
|
|
? AUDIT_PERMISSIONS.enterpriseAssetInfoCardActivity
|
|
: AUDIT_PERMISSIONS.enterpriseAssetInfoDeviceActivity
|
|
: AUDIT_PERMISSIONS.assetInfoEntry
|
|
if (!hasAuth(permission)) return null
|
|
return resolveAuditResourceTarget({
|
|
userType,
|
|
resourceType: props.cardInfo.asset_type === 'card' ? 'iot_card' : 'device',
|
|
internalId: props.cardInfo.asset_id,
|
|
businessIdentifier:
|
|
props.cardInfo.asset_type === 'card' ? props.cardInfo.iccid : props.cardInfo.virtual_no
|
|
})
|
|
})
|
|
const openAssetAudit = () => {
|
|
if (auditTarget.value) openAuditInvestigation(auditTarget.value)
|
|
}
|
|
const getBindingCardAuditTarget = (card: BindingCard) => {
|
|
const userType = Number(userStore.info.user_type)
|
|
const permission =
|
|
userType === 3
|
|
? AUDIT_PERMISSIONS.agentAssetInfoBindingCardActivity
|
|
: userType === 4
|
|
? AUDIT_PERMISSIONS.enterpriseAssetInfoBindingCardActivity
|
|
: AUDIT_PERMISSIONS.assetInfoBindingCardEntry
|
|
if (!hasAuth(permission)) return null
|
|
return resolveAuditResourceTarget({
|
|
userType,
|
|
resourceType: 'iot_card',
|
|
internalId: card.card_id,
|
|
businessIdentifier: card.iccid
|
|
})
|
|
}
|
|
const openBindingCardAudit = (card: BindingCard) => {
|
|
const target = getBindingCardAuditTarget(card)
|
|
if (target) openAuditInvestigation(target)
|
|
}
|
|
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 getCarrierAccountDisplay = (card?: {
|
|
carrier_type?: string
|
|
carrier_name?: string
|
|
}) => {
|
|
if (isAgentOrEnterpriseAccount.value) {
|
|
return getCarrierTypeName(card?.carrier_type)
|
|
}
|
|
return card?.carrier_name || '-'
|
|
}
|
|
|
|
const carrierAccountDisplay = computed(() => getCarrierAccountDisplay(props.cardInfo))
|
|
|
|
const descriptionsColumn = computed(() => {
|
|
if (width.value <= 640) return 1
|
|
if (width.value <= 1024) return 2
|
|
return 5
|
|
})
|
|
|
|
// 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: 'navigateToAsset', identifier: string): void
|
|
}
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
// 使用格式化工具
|
|
const {
|
|
formatDataSize,
|
|
formatDuration,
|
|
getRealNameStatusName,
|
|
getRealNameStatusType,
|
|
getRealnamePolicyName,
|
|
getCarrierTypeName,
|
|
getAssetStatusName,
|
|
getAssetStatusType,
|
|
getOnlineStatusName,
|
|
getOnlineStatusType,
|
|
getSignalQualityText,
|
|
getSignalQualityType,
|
|
getSignalBadReasonText,
|
|
getSwitchModeName
|
|
} = useAssetFormatters()
|
|
|
|
// 排序后的设备卡列表(按卡槽位置排序,当前卡排在最前面)
|
|
type RealtimeDescriptionItem = {
|
|
key: string
|
|
label: string
|
|
value: string
|
|
icon: string
|
|
tagType?: 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
|
span?: number
|
|
}
|
|
|
|
const realtimeDescriptionItems = computed<RealtimeDescriptionItem[]>(() => {
|
|
const realtime = props.deviceRealtime
|
|
|
|
return [
|
|
{
|
|
key: 'status',
|
|
label: '在线状态',
|
|
value: getOnlineStatusName(realtime?.online_status),
|
|
icon: 'online-status',
|
|
tagType: getOnlineStatusType(realtime?.online_status)
|
|
},
|
|
{
|
|
key: 'battery',
|
|
label: '电量',
|
|
icon: 'battery',
|
|
value:
|
|
realtime?.battery_level !== null && realtime?.battery_level !== undefined
|
|
? `${realtime.battery_level}%`
|
|
: '-'
|
|
},
|
|
{
|
|
key: 'boot',
|
|
label: '本次开机时长',
|
|
value: formatDuration(realtime?.run_time),
|
|
icon: 'boot-time'
|
|
},
|
|
{
|
|
key: 'connect',
|
|
label: '本次联网时长',
|
|
value: formatDuration(realtime?.connect_time),
|
|
icon: 'connection-time'
|
|
},
|
|
{
|
|
key: 'online',
|
|
label: '最后在线时间',
|
|
value: formatDateTime(realtime?.last_online_time) || '-',
|
|
icon: 'last-online'
|
|
},
|
|
{
|
|
key: 'realname',
|
|
label: '实名同步时间',
|
|
value: formatAssetInfoTime(props.cardInfo?.last_real_name_check_at),
|
|
icon: 'realname-sync'
|
|
},
|
|
{
|
|
key: 'cardSync',
|
|
label: '卡状态同步时间',
|
|
value: formatAssetInfoTime(props.cardInfo?.last_card_status_check_at),
|
|
icon: 'card-sync'
|
|
},
|
|
{
|
|
key: 'trafficSync',
|
|
label: '流量同步时间',
|
|
value: formatAssetInfoTime(props.cardInfo?.last_data_check_at),
|
|
icon: 'traffic-sync'
|
|
},
|
|
{
|
|
key: 'signal',
|
|
label: '信号综合判断',
|
|
value: getSignalQualityText(realtime?.signal_quality),
|
|
icon: 'signal-quality',
|
|
tagType: getSignalQualityType(realtime?.signal_quality)
|
|
},
|
|
{
|
|
key: 'signalReason',
|
|
label: '信号较弱原因',
|
|
value: getSignalBadReasonText(realtime?.signal_bad_reason),
|
|
icon: 'signal-reason'
|
|
},
|
|
{
|
|
key: 'clients',
|
|
label: '已连接设备数',
|
|
value: `${realtime?.client_number ?? '-'} / ${realtime?.max_clients ?? '-'}`,
|
|
icon: 'connected-clients'
|
|
},
|
|
{
|
|
key: 'wifi',
|
|
label: 'WiFi 状态',
|
|
value:
|
|
realtime?.wifi_enabled === undefined ? '-' : realtime.wifi_enabled ? '已开启' : '已关闭',
|
|
icon: 'wifi-status',
|
|
tagType: realtime?.wifi_enabled ? 'success' : 'info'
|
|
},
|
|
{
|
|
key: 'wifiName',
|
|
label: 'WiFi 名称',
|
|
value: realtime?.ssid || '-',
|
|
icon: 'wifi-name'
|
|
},
|
|
{
|
|
key: 'wifiPassword',
|
|
label: 'WiFi 密码',
|
|
value: realtime?.wifi_password || '-',
|
|
icon: 'wifi-password'
|
|
},
|
|
{ key: 'wan', label: 'WAN IP', value: realtime?.wan_ip || '-', icon: 'wan-ip' },
|
|
{ key: 'lan', label: 'LAN IP', value: realtime?.lan_ip || '-', icon: 'lan-ip' },
|
|
{
|
|
key: 'mac',
|
|
label: 'MAC 地址',
|
|
value: realtime?.mac_address || '-',
|
|
icon: 'mac-address'
|
|
},
|
|
{
|
|
key: 'limit',
|
|
label: '限速',
|
|
value:
|
|
realtime?.limit_speed === undefined
|
|
? '-'
|
|
: realtime.limit_speed
|
|
? `${realtime.limit_speed} KB/s`
|
|
: '不限速',
|
|
icon: 'speed-limit'
|
|
},
|
|
{
|
|
key: 'switchMode',
|
|
label: '切卡模式',
|
|
value:
|
|
realtime?.switch_mode === '0' ? '自动' : realtime?.switch_mode === '1' ? '手动' : '-',
|
|
icon: 'switch-mode'
|
|
},
|
|
{
|
|
key: 'interval',
|
|
label: '上报周期',
|
|
value: realtime?.sync_interval ? `${realtime.sync_interval} 秒` : '-',
|
|
icon: 'report-interval'
|
|
},
|
|
{
|
|
key: 'firmware',
|
|
label: '固件版本',
|
|
value: realtime?.software_version || '-',
|
|
icon: 'firmware-version',
|
|
span: descriptionsColumn.value >= 5 ? 2 : 1
|
|
},
|
|
{
|
|
key: 'updated',
|
|
label: '信息更新时间',
|
|
value: formatDateTime(realtime?.last_update_time) || '-',
|
|
icon: 'realtime-updated'
|
|
}
|
|
]
|
|
})
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* 接口中的这几项时间以其原始文本为基准直接加 8 小时;时区后缀不参与换算。
|
|
* 例如 2026-08-06T11:54:26.693382+08:00 显示为 2026-08-06 19:54:26。
|
|
*/
|
|
const formatAssetInfoTime = (value?: string | null) => {
|
|
if (!value?.trim()) return '-'
|
|
|
|
const timeText = value.trim()
|
|
const match = timeText.match(
|
|
/^(\d{4})-(\d{2})-(\d{2})(?:T|\s)(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/i
|
|
)
|
|
if (!match) return '-'
|
|
|
|
const [, year, month, day, hours, minutes, seconds = '00'] = match
|
|
const date = new Date(
|
|
Date.UTC(
|
|
Number(year),
|
|
Number(month) - 1,
|
|
Number(day),
|
|
Number(hours) + 8,
|
|
Number(minutes),
|
|
Number(seconds)
|
|
)
|
|
)
|
|
|
|
return formatDateTime(date)
|
|
}
|
|
|
|
// 处理轮询开关变化
|
|
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
|
|
emit('navigateToAsset', 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;
|
|
}
|
|
|
|
.expiry-estimate--critical,
|
|
.expiry-estimate--warning,
|
|
.expiry-estimate--notice {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.expiry-estimate--critical {
|
|
color: var(--el-color-danger);
|
|
}
|
|
|
|
.expiry-estimate--warning {
|
|
color: #8e44ad;
|
|
}
|
|
|
|
.expiry-estimate--notice {
|
|
color: #e83e8c;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.info-section-divider {
|
|
display: flex;
|
|
height: 20px;
|
|
align-items: center;
|
|
border-top: 0;
|
|
|
|
:deep(.el-divider__text) {
|
|
position: static;
|
|
display: inline-flex;
|
|
padding: 0;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
line-height: 20px;
|
|
color: var(--el-text-color-primary);
|
|
background-color: transparent;
|
|
transform: none;
|
|
}
|
|
|
|
.section-divider-title {
|
|
display: inline-flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
|
|
&::before {
|
|
width: 3px;
|
|
height: 16px;
|
|
content: '';
|
|
background-color: var(--el-color-primary);
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.asset-descriptions {
|
|
:deep(.el-descriptions__label.is-vertical-label) {
|
|
padding: 8px 15px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
line-height: 18px;
|
|
color: var(--el-text-color-secondary);
|
|
background-color: var(--el-fill-color-extra-light);
|
|
}
|
|
|
|
:deep(.asset-label::before) {
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
margin-right: 6px;
|
|
font-family: iconfont-sys, sans-serif !important;
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
vertical-align: -3px;
|
|
color: var(--el-color-primary);
|
|
content: '';
|
|
opacity: 0.82;
|
|
}
|
|
|
|
:deep(.asset-label--identifier-iccid::before) {
|
|
content: '\e81a';
|
|
}
|
|
|
|
:deep(.asset-label--identifier-msisdn::before) {
|
|
content: '\e776';
|
|
}
|
|
|
|
:deep(.asset-label--operator-account::before) {
|
|
content: '\e70d';
|
|
}
|
|
|
|
:deep(.asset-label--realname-status::before) {
|
|
color: var(--el-color-success);
|
|
content: '\e88b';
|
|
}
|
|
|
|
:deep(.asset-label--realname-policy::before) {
|
|
color: var(--el-color-warning);
|
|
content: '\e75d';
|
|
}
|
|
|
|
:deep(.asset-label--shop::before) {
|
|
content: '\e737';
|
|
}
|
|
|
|
:deep(.asset-label--package-series::before) {
|
|
content: '\e762';
|
|
}
|
|
|
|
:deep(.asset-label--virtual-number::before) {
|
|
content: '\e7bd';
|
|
}
|
|
|
|
:deep(.asset-label--network-status::before) {
|
|
content: '\e757';
|
|
}
|
|
|
|
:deep(.asset-label--usage-month::before) {
|
|
content: '\e812';
|
|
}
|
|
|
|
:deep(.asset-label--usage-cycle::before) {
|
|
content: '\e82a';
|
|
}
|
|
|
|
:deep(.asset-label--realname-time::before) {
|
|
content: '\e6f7';
|
|
}
|
|
|
|
:deep(.asset-label--realname-sync::before) {
|
|
content: '\e793';
|
|
}
|
|
|
|
:deep(.asset-label--identifier-imei::before) {
|
|
content: '\e8d3';
|
|
}
|
|
|
|
:deep(.asset-label--asset-status::before) {
|
|
color: var(--el-color-success);
|
|
content: '\e88c';
|
|
}
|
|
|
|
:deep(.asset-label--card-sync::before) {
|
|
content: '\e7d3';
|
|
}
|
|
|
|
:deep(.asset-label--traffic-sync::before) {
|
|
content: '\e7e1';
|
|
}
|
|
|
|
:deep(.asset-label--operator-status::before) {
|
|
content: '\e871';
|
|
}
|
|
|
|
:deep(.asset-label--package-used::before) {
|
|
content: '\e742';
|
|
}
|
|
|
|
:deep(.asset-label--package-remaining::before) {
|
|
content: '\e7d9';
|
|
}
|
|
|
|
:deep(.asset-label--package-expire-estimate::before) {
|
|
content: '\e6f3';
|
|
}
|
|
|
|
:deep(.asset-label--bound-device-number::before) {
|
|
content: '\e851';
|
|
}
|
|
|
|
:deep(.asset-label--bound-device-name::before) {
|
|
content: '\e7d8';
|
|
}
|
|
|
|
:deep(.asset-label--device-name::before) {
|
|
content: '\e7d8';
|
|
}
|
|
|
|
:deep(.asset-label--device-status::before) {
|
|
color: var(--el-color-success);
|
|
content: '\e708';
|
|
}
|
|
|
|
:deep(.asset-label--activation-status::before) {
|
|
color: var(--el-color-warning);
|
|
content: '\e81c';
|
|
}
|
|
|
|
:deep(.asset-label--device-model::before) {
|
|
content: '\e7a7';
|
|
}
|
|
|
|
:deep(.asset-label--online-status::before) {
|
|
content: '\e6b2';
|
|
}
|
|
|
|
:deep(.asset-label--switch-mode::before) {
|
|
content: '\e72b';
|
|
}
|
|
|
|
:deep(.asset-label--bound-card-count::before) {
|
|
content: '\e730';
|
|
}
|
|
|
|
:deep(.asset-label--max-slots::before) {
|
|
content: '\e620';
|
|
}
|
|
|
|
:deep(.asset-label--device-type::before) {
|
|
content: '\e740';
|
|
}
|
|
|
|
:deep(.asset-label--manufacturer::before) {
|
|
content: '\e8a6';
|
|
}
|
|
|
|
:deep(.asset-label--last-online::before) {
|
|
content: '\e751';
|
|
}
|
|
|
|
:deep(.asset-label--identifier-sn::before) {
|
|
content: '\e72a';
|
|
}
|
|
|
|
:deep(.asset-label--last-sync::before) {
|
|
content: '\e614';
|
|
}
|
|
|
|
:deep(.asset-label--battery::before) {
|
|
color: var(--el-color-success);
|
|
content: '\e6e5';
|
|
}
|
|
|
|
:deep(.asset-label--boot-time::before) {
|
|
content: '\e827';
|
|
}
|
|
|
|
:deep(.asset-label--connection-time::before) {
|
|
content: '\e790';
|
|
}
|
|
|
|
:deep(.asset-label--signal-quality::before) {
|
|
content: '\e808';
|
|
}
|
|
|
|
:deep(.asset-label--signal-reason::before) {
|
|
color: var(--el-color-warning);
|
|
content: '\e600';
|
|
}
|
|
|
|
:deep(.asset-label--connected-clients::before) {
|
|
content: '\e608';
|
|
}
|
|
|
|
:deep(.asset-label--wifi-status::before) {
|
|
content: '\e7ee';
|
|
}
|
|
|
|
:deep(.asset-label--wifi-name::before) {
|
|
content: '\e841';
|
|
}
|
|
|
|
:deep(.asset-label--wifi-password::before) {
|
|
color: var(--el-color-warning);
|
|
content: '\e716';
|
|
}
|
|
|
|
:deep(.asset-label--wan-ip::before) {
|
|
content: '\e611';
|
|
}
|
|
|
|
:deep(.asset-label--lan-ip::before) {
|
|
content: '\e607';
|
|
}
|
|
|
|
:deep(.asset-label--mac-address::before) {
|
|
content: '\e68b';
|
|
}
|
|
|
|
:deep(.asset-label--speed-limit::before) {
|
|
content: '\e827';
|
|
}
|
|
|
|
:deep(.asset-label--report-interval::before) {
|
|
content: '\e7ac';
|
|
}
|
|
|
|
:deep(.asset-label--firmware-version::before) {
|
|
content: '\e726';
|
|
}
|
|
|
|
:deep(.asset-label--realtime-updated::before) {
|
|
content: '\e6b3';
|
|
}
|
|
|
|
:deep(.el-descriptions__content.is-vertical-content) {
|
|
padding: 8px 15px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
line-height: 23px;
|
|
color: var(--el-text-color-primary);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
}
|
|
|
|
.binding-card-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.binding-card-list--1 {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.binding-card-list--2 {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.binding-card {
|
|
min-width: 0;
|
|
}
|
|
|
|
.binding-card-header,
|
|
.binding-card-identifier,
|
|
.binding-card-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.binding-card-header {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.binding-card-details {
|
|
margin-top: 12px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--el-border-color);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.binding-card-field {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
min-height: 42px;
|
|
min-width: 0;
|
|
padding: 0 12px;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
|
&:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
}
|
|
|
|
.binding-card-field-label {
|
|
flex: 0 0 auto;
|
|
font-size: 14px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.binding-card-field-value {
|
|
min-width: 0;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: var(--el-text-color-primary);
|
|
text-align: right;
|
|
}
|
|
|
|
.binding-card--current {
|
|
border-color: var(--el-color-primary);
|
|
box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
|
|
}
|
|
|
|
.binding-card-current-tag {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.binding-card-identifier {
|
|
min-width: 0;
|
|
|
|
.el-button {
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
line-height: 24px;
|
|
}
|
|
|
|
.copy-icon {
|
|
flex: 0 0 auto;
|
|
margin-left: 6px;
|
|
color: var(--el-color-primary);
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.binding-card-actions {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
gap: 8px;
|
|
padding-top: 12px;
|
|
margin-top: 12px;
|
|
|
|
.el-button {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
width: 100%;
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
|
|
.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) {
|
|
.binding-card-list {
|
|
grid-template-columns: 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.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>
|