This commit is contained in:
4
src/types/components.d.ts
vendored
4
src/types/components.d.ts
vendored
@@ -95,6 +95,7 @@ declare module 'vue' {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
@@ -124,12 +125,15 @@ declare module 'vue' {
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -453,8 +453,8 @@
|
||||
<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 :type="getNetworkStatusType(bindingCard.network_status)" size="small">
|
||||
{{ getNetworkStatusName(bindingCard.network_status) }}
|
||||
</ElTag>
|
||||
</span>
|
||||
</div>
|
||||
@@ -486,30 +486,28 @@
|
||||
|
||||
<div class="binding-card-actions">
|
||||
<ElButton
|
||||
v-if="bindingCard.network_status === 0"
|
||||
v-if="bindingCard.network_status === 0 && hasAuth('iot_info:start')"
|
||||
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"
|
||||
v-if="bindingCard.network_status === 1 && hasAuth('iot_info:stop')"
|
||||
type="warning"
|
||||
size="default"
|
||||
:disabled="isBindingStopDisabled(bindingCard)"
|
||||
@click="handleDisableBindingCard(bindingCard)"
|
||||
v-permission="'iot_info:stop'"
|
||||
>
|
||||
{{ getBindingStopButtonText(bindingCard) }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="hasAuth('bound_card:update_realname_status')"
|
||||
type="primary"
|
||||
size="default"
|
||||
@click="handleUpdateBindingCardRealnameStatus(bindingCard)"
|
||||
v-permission="'bound_card:update_realname_status'"
|
||||
>
|
||||
更新实名状态
|
||||
</ElButton>
|
||||
@@ -580,8 +578,7 @@
|
||||
style="margin-top: 16px; text-align: right"
|
||||
>
|
||||
<ElButton
|
||||
v-if="cardInfo?.network_status === 0"
|
||||
v-permission="'iot_info:start'"
|
||||
v-if="cardInfo?.network_status === 0 && hasAuth('iot_info:start')"
|
||||
type="success"
|
||||
:disabled="startDisabled"
|
||||
@click="handleEnableCard"
|
||||
@@ -589,8 +586,7 @@
|
||||
{{ startRemainingText ? `启用此卡(${startRemainingText})` : '启用此卡' }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="cardInfo?.network_status === 1"
|
||||
v-permission="'iot_info:stop'"
|
||||
v-if="cardInfo?.network_status === 1 && hasAuth('iot_info:stop')"
|
||||
type="warning"
|
||||
:disabled="stopDisabled"
|
||||
@click="handleDisableCard"
|
||||
@@ -601,15 +597,14 @@
|
||||
<!-- 手动停用暂时不开放 -->
|
||||
<!--suppress VueUnrecognizedDirective -->
|
||||
<ElButton
|
||||
v-if="canShowRealnamePolicy"
|
||||
v-permission="'asset:card_realname_policy'"
|
||||
v-if="canShowRealnamePolicy && hasAuth('asset:card_realname_policy')"
|
||||
type="primary"
|
||||
@click="handleShowRealnamePolicy"
|
||||
>
|
||||
实名认证策略
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'iot_info:update_realname_status'"
|
||||
v-if="hasAuth('iot_info:update_realname_status')"
|
||||
type="primary"
|
||||
@click="handleShowUpdateRealnameStatus"
|
||||
>
|
||||
@@ -619,28 +614,27 @@
|
||||
|
||||
<!-- 设备操作按钮 -->
|
||||
<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 v-if="hasAuth('device_info:switch_mode')" type="info" @click="handleShowSwitchMode"
|
||||
>设置切卡模式</ElButton
|
||||
>
|
||||
<ElButton v-permission="'device_info:reboot'" type="primary" @click="handleRebootDevice">
|
||||
<ElButton v-if="hasAuth('device_info:reboot')" type="primary" @click="handleRebootDevice">
|
||||
重启设备
|
||||
</ElButton>
|
||||
<ElButton v-permission="'device_info:reset'" type="warning" @click="handleResetDevice">
|
||||
<ElButton v-if="hasAuth('device_info:reset')" type="warning" @click="handleResetDevice">
|
||||
恢复出厂
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'device_info:switch_card'"
|
||||
v-if="hasAuth('device_info:switch_card')"
|
||||
type="success"
|
||||
@click="handleShowSwitchCard"
|
||||
>
|
||||
切换SIM卡
|
||||
</ElButton>
|
||||
<ElButton v-permission="'device_info:set_wifi'" type="primary" @click="handleShowSetWiFi">
|
||||
<ElButton v-if="hasAuth('device_info:set_wifi')" type="primary" @click="handleShowSetWiFi">
|
||||
设置WiFi
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="canShowRealnamePolicy"
|
||||
v-permission="'asset_info:device_realname_policy'"
|
||||
v-if="canShowRealnamePolicy && hasAuth('asset_info:device_realname_policy')"
|
||||
type="primary"
|
||||
@click="handleShowRealnamePolicy"
|
||||
>
|
||||
@@ -944,6 +938,8 @@
|
||||
formatDuration,
|
||||
getRealNameStatusName,
|
||||
getRealNameStatusType,
|
||||
getNetworkStatusName,
|
||||
getNetworkStatusType,
|
||||
getRealnamePolicyName,
|
||||
getCarrierTypeName,
|
||||
getAssetStatusName,
|
||||
|
||||
@@ -68,6 +68,19 @@ export function useAssetFormatters() {
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取网络状态名称和标签类型,缺失状态不能误报为停机。
|
||||
const getNetworkStatusName = (status?: number) => {
|
||||
if (status === 1) return '正常'
|
||||
if (status === 0) return '停机'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
const getNetworkStatusType = (status?: number): TagType => {
|
||||
if (status === 1) return 'success'
|
||||
if (status === 0) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// 获取实名认证策略名称
|
||||
const getRealnamePolicyName = (policy?: string) => {
|
||||
if (!policy || policy === '') return '未知'
|
||||
@@ -261,6 +274,8 @@ export function useAssetFormatters() {
|
||||
formatIccidWithDashes,
|
||||
getRealNameStatusName,
|
||||
getRealNameStatusType,
|
||||
getNetworkStatusName,
|
||||
getNetworkStatusType,
|
||||
getRealnamePolicyName,
|
||||
getCarrierTypeName,
|
||||
getAssetStatusName,
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* 负责资产详情数据的加载和状态管理
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { getCurrentScope, onScopeDispose, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
import type {
|
||||
AssetBoundCard,
|
||||
@@ -25,13 +26,19 @@ const normalizeOptionalText = (value?: string | null) =>
|
||||
typeof value === 'string' ? value.trim() : ''
|
||||
|
||||
const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
||||
'gateway_extend',
|
||||
'last_data_check_at',
|
||||
'last_real_name_check_at',
|
||||
'last_card_status_check_at'
|
||||
])
|
||||
|
||||
// 详情接口是绑定卡实名时间的唯一来源;实时状态接口不应覆盖或保留旧值。
|
||||
const DETAIL_ONLY_BOUND_CARD_FIELDS = new Set(['real_name_at'])
|
||||
// 绑定关系和静态信息以详情接口为准,实时接口只补充同一张卡的状态。
|
||||
const REALTIME_BOUND_CARD_FIELDS = new Set([
|
||||
'network_status',
|
||||
'real_name_status',
|
||||
'is_current',
|
||||
...NULLABLE_REALTIME_CARD_FIELDS
|
||||
])
|
||||
|
||||
/**
|
||||
* 格式化数据大小(MB -> GB/MB)
|
||||
@@ -60,22 +67,24 @@ const getDisplayedUsagePercentage = (traffic?: TrafficDisplaySource | null) => {
|
||||
const mergeAssetBoundCards = (
|
||||
existingCards: AssetBoundCard[] = [],
|
||||
realtimeCards: AssetBoundCard[] = []
|
||||
): AssetBoundCard[] =>
|
||||
realtimeCards.map((realtimeCard) => {
|
||||
const existingCard = existingCards.find((card) => card.iccid === realtimeCard.iccid)
|
||||
if (!existingCard) {
|
||||
return { ...realtimeCard, real_name_at: null }
|
||||
}
|
||||
): AssetBoundCard[] => {
|
||||
const realtimeByIccid = new Map(realtimeCards.map((card) => [card.iccid, card]))
|
||||
const currentCard = existingCards.find(
|
||||
(card) => realtimeByIccid.get(card.iccid)?.is_current === true
|
||||
)
|
||||
|
||||
return existingCards.map((existingCard) => {
|
||||
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
||||
for (const [key, value] of Object.entries(realtimeCard)) {
|
||||
if (DETAIL_ONLY_BOUND_CARD_FIELDS.has(key)) continue
|
||||
for (const [key, value] of Object.entries(realtimeByIccid.get(existingCard.iccid) || {})) {
|
||||
if (!REALTIME_BOUND_CARD_FIELDS.has(key)) continue
|
||||
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
||||
mergedCard[key] = value
|
||||
}
|
||||
}
|
||||
if (currentCard) mergedCard.is_current = existingCard.iccid === currentCard.iccid
|
||||
return mergedCard as unknown as AssetBoundCard
|
||||
})
|
||||
}
|
||||
|
||||
export function useAssetInfo() {
|
||||
// 状态管理
|
||||
@@ -90,22 +99,93 @@ export function useAssetInfo() {
|
||||
const walletLoading = ref(false)
|
||||
const packageLoading = ref(false)
|
||||
const realtimeLoading = ref(false)
|
||||
let assetVersion = 0
|
||||
let packageRequestId = 0
|
||||
let currentPackageRequestId = 0
|
||||
let realtimeRequestId = 0
|
||||
let walletRequestId = 0
|
||||
let disposed = false
|
||||
|
||||
const resetAssetInfo = () => {
|
||||
assetVersion += 1
|
||||
cardInfo.value = null
|
||||
deviceRealtime.value = null
|
||||
currentPackage.value = null
|
||||
currentPackageErrorMsg.value = ''
|
||||
packageList.value = []
|
||||
walletInfo.value = null
|
||||
loading.value = false
|
||||
currentPackageLoading.value = false
|
||||
packageLoading.value = false
|
||||
realtimeLoading.value = false
|
||||
walletLoading.value = false
|
||||
}
|
||||
|
||||
// 同一标识符也可能属于不同搜索或登录会话,不能仅比较 identifier。
|
||||
const captureAssetRequest = (identifier: string) => {
|
||||
const version = assetVersion
|
||||
const asset = cardInfo.value
|
||||
return () =>
|
||||
!disposed &&
|
||||
version === assetVersion &&
|
||||
asset !== null &&
|
||||
cardInfo.value === asset &&
|
||||
asset.identifier === identifier
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
watch(
|
||||
[
|
||||
() => userStore.info.id,
|
||||
() => userStore.info.user_type,
|
||||
() => userStore.info.shop_id,
|
||||
() => userStore.info.enterprise_id,
|
||||
() => userStore.isLogin
|
||||
],
|
||||
resetAssetInfo,
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
if (getCurrentScope()) {
|
||||
onScopeDispose(() => {
|
||||
disposed = true
|
||||
resetAssetInfo()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产详情
|
||||
* @param identifier - 资产标识符(ICCID、IMEI、SN、虚拟号或MSISDN)
|
||||
*/
|
||||
const fetchAssetDetail = async (identifier: string) => {
|
||||
const fetchAssetDetail = async (
|
||||
identifier: string,
|
||||
{ preservePackageList = false }: { preservePackageList?: boolean } = {}
|
||||
) => {
|
||||
if (disposed) return null
|
||||
const previousAsset = cardInfo.value
|
||||
const previousPackages =
|
||||
preservePackageList && cardInfo.value?.identifier === identifier ? packageList.value : []
|
||||
resetAssetInfo()
|
||||
packageList.value = previousPackages
|
||||
const version = assetVersion
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await AssetService.resolveAsset(identifier, { include_usage_summary: true })
|
||||
if (version !== assetVersion) return null
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
const data = response.data
|
||||
const resolvedGatewayCardImei = normalizeOptionalText(data.gateway_card_imei)
|
||||
|
||||
// 获取资产标识符
|
||||
const assetIdentifier = data.asset_type === 'card' ? data.iccid : data.virtual_no
|
||||
const assetIdentifier =
|
||||
(data.asset_type === 'card' ? data.iccid : data.virtual_no) || identifier
|
||||
if (
|
||||
previousAsset?.asset_type !== data.asset_type ||
|
||||
previousAsset?.asset_id !== data.asset_id ||
|
||||
previousAsset?.identifier !== assetIdentifier
|
||||
) {
|
||||
packageList.value = []
|
||||
}
|
||||
|
||||
// 映射API数据到页面显示格式
|
||||
cardInfo.value = {
|
||||
@@ -168,7 +248,7 @@ export function useAssetInfo() {
|
||||
sn: data.sn || '',
|
||||
max_sim_slots: data.max_sim_slots || 0,
|
||||
bound_card_count: data.bound_card_count || 0,
|
||||
cards: data.cards || [],
|
||||
cards: (data.cards || []).map((card) => ({ ...card })),
|
||||
device_protect_status: data.device_protect_status,
|
||||
online_status: data.online_status,
|
||||
last_online_time: data.last_online_time,
|
||||
@@ -196,15 +276,17 @@ export function useAssetInfo() {
|
||||
loadAssetWallet(assetIdentifier)
|
||||
])
|
||||
}
|
||||
return version === assetVersion ? cardInfo.value : null
|
||||
} else {
|
||||
ElMessage.error(response.msg || '查询失败')
|
||||
cardInfo.value = null
|
||||
}
|
||||
} catch {
|
||||
cardInfo.value = null
|
||||
if (version === assetVersion) resetAssetInfo()
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (version === assetVersion) loading.value = false
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,14 +295,24 @@ export function useAssetInfo() {
|
||||
* @param page - 页码
|
||||
* @param pageSize - 每页条数
|
||||
*/
|
||||
const loadPackageList = async (identifier: string, page: number = 1, pageSize: number = 10, status?: number) => {
|
||||
const loadPackageList = async (
|
||||
identifier: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 10,
|
||||
status?: number
|
||||
) => {
|
||||
const isCurrentAsset = captureAssetRequest(identifier)
|
||||
if (!isCurrentAsset()) return null
|
||||
const requestId = ++packageRequestId
|
||||
const isCurrent = () => isCurrentAsset() && requestId === packageRequestId
|
||||
try {
|
||||
packageLoading.value = true
|
||||
const response = await AssetService.getAssetPackages(identifier, {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
status
|
||||
page_size: pageSize,
|
||||
status
|
||||
})
|
||||
if (!isCurrent()) return null
|
||||
if (response.code === 0 && response.data) {
|
||||
// items 代表顶层主套餐关系组,必须保留其 children,不得展平。
|
||||
const items = response.data.items || []
|
||||
@@ -233,12 +325,15 @@ export function useAssetInfo() {
|
||||
pageSize: response.data.page_size || pageSize
|
||||
}
|
||||
}
|
||||
packageList.value = []
|
||||
return { items: [], total: 0, page: 1, pageSize }
|
||||
} catch (error) {
|
||||
if (!isCurrent()) return null
|
||||
console.error('获取套餐列表失败:', error)
|
||||
packageList.value = []
|
||||
return { items: [], total: 0, page: 1, pageSize }
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
if (isCurrent()) packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,11 +342,16 @@ export function useAssetInfo() {
|
||||
* @param identifier - 资产标识符
|
||||
*/
|
||||
const loadCurrentPackage = async (identifier: string) => {
|
||||
const isCurrentAsset = captureAssetRequest(identifier)
|
||||
if (!isCurrentAsset()) return
|
||||
const requestId = ++currentPackageRequestId
|
||||
const isCurrent = () => isCurrentAsset() && requestId === currentPackageRequestId
|
||||
try {
|
||||
currentPackageLoading.value = true
|
||||
currentPackageErrorMsg.value = ''
|
||||
|
||||
const response = await AssetService.getCurrentPackage(identifier)
|
||||
if (!isCurrent()) return
|
||||
|
||||
if (response.code === 0) {
|
||||
if (response.data) {
|
||||
@@ -300,6 +400,7 @@ export function useAssetInfo() {
|
||||
currentPackageErrorMsg.value = response.msg || '获取当前套餐失败'
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (!isCurrent()) return
|
||||
// 404表示无当前生效套餐,这是正常情况
|
||||
currentPackage.value = null
|
||||
if (error?.response?.status === 404) {
|
||||
@@ -309,7 +410,7 @@ export function useAssetInfo() {
|
||||
currentPackageErrorMsg.value = error?.message || '获取当前套餐失败'
|
||||
}
|
||||
} finally {
|
||||
currentPackageLoading.value = false
|
||||
if (isCurrent()) currentPackageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,11 +420,22 @@ export function useAssetInfo() {
|
||||
* @param assetType - 资产类型(card/device)
|
||||
*/
|
||||
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
|
||||
const isCurrentAsset = captureAssetRequest(identifier)
|
||||
if (!isCurrentAsset() || cardInfo.value.asset_type !== assetType) return
|
||||
const requestId = ++realtimeRequestId
|
||||
const isCurrent = () => isCurrentAsset() && requestId === realtimeRequestId
|
||||
try {
|
||||
realtimeLoading.value = true
|
||||
const response = await AssetService.getRealtimeStatus(identifier)
|
||||
if (!isCurrent()) return
|
||||
if (response.code === 0 && response.data) {
|
||||
const data = response.data
|
||||
if (
|
||||
(data.asset_type !== undefined && data.asset_type !== assetType) ||
|
||||
(data.asset_id !== undefined && String(data.asset_id) !== String(cardInfo.value.asset_id))
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 更新实时状态数据
|
||||
if (assetType === 'card') {
|
||||
// 卡的实时状态
|
||||
@@ -364,8 +476,7 @@ export function useAssetInfo() {
|
||||
cardInfo.value.device_protect_status = data.device_protect_status
|
||||
}
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
// 合并实时卡数据与原始卡数据,保留 real_name_at 等静态字段。
|
||||
// 设备详情里的实名状态、实名时间依赖选中的绑定卡,因此不能在实时刷新时丢掉这些字段。
|
||||
// 实时 cards 可能缺少部分卡或静态字段,不能替换详情里的完整绑定列表。
|
||||
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
||||
}
|
||||
if (data.last_data_check_at !== undefined) {
|
||||
@@ -399,9 +510,9 @@ export function useAssetInfo() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取实时状态失败:', error)
|
||||
if (isCurrent()) console.error('获取实时状态失败:', error)
|
||||
} finally {
|
||||
realtimeLoading.value = false
|
||||
if (isCurrent()) realtimeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,13 +521,22 @@ export function useAssetInfo() {
|
||||
* @param identifier - 资产标识符
|
||||
*/
|
||||
const loadAssetWallet = async (identifier: string) => {
|
||||
const isCurrentAsset = captureAssetRequest(identifier)
|
||||
if (!isCurrentAsset()) return
|
||||
const requestId = ++walletRequestId
|
||||
const isCurrent = () => isCurrentAsset() && requestId === walletRequestId
|
||||
try {
|
||||
walletLoading.value = true
|
||||
const response = await AssetService.getAssetWallet(identifier)
|
||||
if (!isCurrent()) return
|
||||
if (response.code === 0 && response.data) {
|
||||
walletInfo.value = response.data
|
||||
} else {
|
||||
walletInfo.value = null
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (!isCurrent()) return
|
||||
walletInfo.value = null
|
||||
if (error?.response?.status === 403) {
|
||||
// 企业账号禁止查看钱包信息,不显示错误
|
||||
walletInfo.value = null
|
||||
@@ -424,7 +544,7 @@ export function useAssetInfo() {
|
||||
console.error('获取钱包概况失败:', error)
|
||||
}
|
||||
} finally {
|
||||
walletLoading.value = false
|
||||
if (isCurrent()) walletLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,16 +555,18 @@ export function useAssetInfo() {
|
||||
* @returns true if refresh was successful, false if rate limited
|
||||
*/
|
||||
const refreshAsset = async (identifier: string, assetType: string): Promise<boolean> => {
|
||||
const isCurrent = captureAssetRequest(identifier)
|
||||
if (!isCurrent() || cardInfo.value.asset_type !== assetType) return false
|
||||
try {
|
||||
// 主动同步网关的接口仅允许由右上角“同步”按钮触发,搜索和普通数据重载不能调用。
|
||||
await AssetService.refreshAsset(identifier)
|
||||
if (!isCurrent()) return false
|
||||
ElMessage.success('刷新成功')
|
||||
|
||||
// 刷新成功后重新加载实时状态数据
|
||||
if (identifier) {
|
||||
await loadRealtimeStatus(identifier, assetType)
|
||||
}
|
||||
return true
|
||||
// 绑定/解绑和实名时间由详情接口确认,不能只刷新实时状态而保留过期绑定关系。
|
||||
return Boolean(await fetchAssetDetail(identifier, { preservePackageList: true }))
|
||||
} catch (error: any) {
|
||||
if (!isCurrent()) return false
|
||||
if (error instanceof FrontendRateLimitError) {
|
||||
ElMessage.warning('操作过于频繁,请稍后重试')
|
||||
return false
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<!-- 第一层:资产概览与当前生效套餐,按信息高度纵向展开 -->
|
||||
<div class="row overview-primary">
|
||||
<BasicInfoCard
|
||||
:key="`${cardInfo.asset_type}:${cardInfo.asset_id}`"
|
||||
:card-info="cardInfo"
|
||||
:device-realtime="deviceRealtime"
|
||||
v-model:polling-enabled="pollingEnabled"
|
||||
@@ -295,6 +296,8 @@
|
||||
const speedTierSubmitting = ref(false)
|
||||
const bindingCardRealnameStatusIccid = ref('')
|
||||
const bindingCardRealnameStatusValue = ref<number>(0)
|
||||
let realnameStatusAsset: any = null
|
||||
let bindingCardRealnameStatusAsset: any = null
|
||||
|
||||
const switchModeCards = computed<SwitchModeCardOption[]>(() =>
|
||||
(cardInfo.value?.cards || [])
|
||||
@@ -402,34 +405,33 @@
|
||||
/**
|
||||
* 处理资产搜索
|
||||
*/
|
||||
let searchRequestId = 0
|
||||
const handleSearch = async ({ identifier }: { identifier: string }) => {
|
||||
const requestId = ++searchRequestId
|
||||
// 更新搜索框的值
|
||||
assetSearchCardRef.value?.updateSearchValue(identifier)
|
||||
startFailureMessage.value = ''
|
||||
packagePagination.value.page = 1
|
||||
packagePagination.value.total = 0
|
||||
packagePagination.value.status = undefined
|
||||
|
||||
// fetchAssetDetail 内部已经会并行加载所有相关数据,不需要重复调用
|
||||
await fetchAssetDetail(identifier)
|
||||
|
||||
// 如果是设备类型,需要再次调用搜索接口获取完整数据
|
||||
if (cardInfo.value?.asset_type === 'device') {
|
||||
const deviceIdentifier = cardInfo.value.virtual_no || cardInfo.value.iccid || identifier
|
||||
await fetchAssetDetail(deviceIdentifier)
|
||||
}
|
||||
|
||||
if (cardInfo.value) {
|
||||
walletTransactionSearchToken.value += 1
|
||||
}
|
||||
// resolve 已返回完整详情;设备不再重复搜索,避免多轮响应相互覆盖。
|
||||
const asset = await fetchAssetDetail(identifier)
|
||||
if (requestId !== searchRequestId || !asset || cardInfo.value !== asset) return
|
||||
walletTransactionSearchToken.value += 1
|
||||
|
||||
// 从接口数据初始化轮询开关,不触发更新接口
|
||||
pollingInitializing = true
|
||||
pollingEnabled.value = cardInfo.value?.enable_polling ?? false
|
||||
pollingEnabled.value = asset.enable_polling ?? false
|
||||
await nextTick()
|
||||
pollingInitializing = false
|
||||
if (requestId !== searchRequestId || cardInfo.value !== asset) return
|
||||
|
||||
// 重置分页并加载套餐列表
|
||||
packagePagination.value.page = 1
|
||||
const result = await loadPackageList(identifier, 1, packagePagination.value.pageSize)
|
||||
packagePagination.value.total = result.total
|
||||
// 套餐也使用解析后的规范标识符,支持通过 ICCID、IMEI、SN 等入口查同一设备。
|
||||
const result = await loadPackageList(asset.identifier, 1, packagePagination.value.pageSize)
|
||||
if (result && requestId === searchRequestId && cardInfo.value === asset) {
|
||||
packagePagination.value.total = result.total
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,7 +457,7 @@
|
||||
try {
|
||||
const started = await enableCardOp()
|
||||
if (started && cardInfo.value) {
|
||||
await fetchAssetDetail(cardInfo.value.identifier)
|
||||
await fetchAssetDetail(cardInfo.value.identifier, { preservePackageList: true })
|
||||
}
|
||||
} finally {
|
||||
updateRateLimitNow()
|
||||
@@ -568,18 +570,24 @@
|
||||
* 显示更新实名状态对话框
|
||||
*/
|
||||
const showUpdateRealnameStatusDialog = () => {
|
||||
realnameStatusAsset = cardInfo.value
|
||||
updateRealnameStatusDialogVisible.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实名状态成功
|
||||
*/
|
||||
const handleUpdateRealnameStatusSuccess = async () => {}
|
||||
const handleUpdateRealnameStatusSuccess = async () => {
|
||||
if (realnameStatusAsset && cardInfo.value === realnameStatusAsset) {
|
||||
await fetchAssetDetail(realnameStatusAsset.identifier, { preservePackageList: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示绑定卡更新实名状态对话框
|
||||
*/
|
||||
const handleUpdateBindingCardRealnameStatus = (payload: { card: any }) => {
|
||||
bindingCardRealnameStatusAsset = cardInfo.value
|
||||
bindingCardRealnameStatusIccid.value = payload.card.iccid
|
||||
bindingCardRealnameStatusValue.value = payload.card.real_name_status ?? 0
|
||||
bindingCardRealnameStatusDialogVisible.value = true
|
||||
@@ -588,7 +596,11 @@
|
||||
/**
|
||||
* 绑定卡更新实名状态成功
|
||||
*/
|
||||
const handleBindingCardRealnameStatusSuccess = async () => {}
|
||||
const handleBindingCardRealnameStatusSuccess = async () => {
|
||||
if (bindingCardRealnameStatusAsset && cardInfo.value === bindingCardRealnameStatusAsset) {
|
||||
await fetchAssetDetail(bindingCardRealnameStatusAsset.identifier, { preservePackageList: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认实名认证策略
|
||||
@@ -676,6 +688,7 @@
|
||||
packagePagination.value.page,
|
||||
packagePagination.value.pageSize
|
||||
)
|
||||
if (!result) return
|
||||
packagePagination.value.total = result.total
|
||||
await Promise.all([
|
||||
loadCurrentPackage(cardInfo.value.identifier),
|
||||
@@ -696,7 +709,7 @@
|
||||
page,
|
||||
packagePagination.value.pageSize
|
||||
)
|
||||
packagePagination.value.total = result.total
|
||||
if (result) packagePagination.value.total = result.total
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -707,7 +720,7 @@
|
||||
packagePagination.value.pageSize = size
|
||||
packagePagination.value.page = 1 // 重置到第一页
|
||||
const result = await loadPackageList(cardInfo.value.identifier, 1, size)
|
||||
packagePagination.value.total = result.total
|
||||
if (result) packagePagination.value.total = result.total
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,7 +733,7 @@
|
||||
packagePagination.value.page,
|
||||
packagePagination.value.pageSize
|
||||
)
|
||||
packagePagination.value.total = result.total
|
||||
if (result) packagePagination.value.total = result.total
|
||||
}
|
||||
|
||||
const handlePackageStatusChange = async (status: number | undefined) => {
|
||||
@@ -733,7 +746,7 @@
|
||||
packagePagination.value.pageSize,
|
||||
status
|
||||
)
|
||||
packagePagination.value.total = result.total
|
||||
if (result) packagePagination.value.total = result.total
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1618,6 +1618,7 @@
|
||||
{ label: 'IMEI', prop: 'imei' },
|
||||
{ label: '设备名称', prop: 'device_name' },
|
||||
{ label: '关联手机号', prop: 'associated_phones' },
|
||||
{ label: '授权企业', prop: 'authorized_enterprise_name' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '套餐系列', prop: 'series_name' },
|
||||
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
|
||||
@@ -1903,6 +1904,13 @@
|
||||
formatter: (row: Device) =>
|
||||
row.associated_phones?.length ? row.associated_phones.join('、') : '-'
|
||||
},
|
||||
{
|
||||
prop: 'authorized_enterprise_name',
|
||||
label: '授权企业',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: Device) => row.authorized_enterprise_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
|
||||
@@ -1949,6 +1949,7 @@
|
||||
{ label: '关联手机号', prop: 'associated_phones' },
|
||||
{ label: '卡虚拟号', prop: 'virtual_no' },
|
||||
{ label: '运营商', prop: 'carrier_name' },
|
||||
{ label: '授权企业', prop: 'authorized_enterprise_name' },
|
||||
{ label: '卡业务类型', prop: 'card_category' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '资产世代', prop: 'generation' },
|
||||
@@ -2128,6 +2129,13 @@
|
||||
width: 150,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'authorized_enterprise_name',
|
||||
label: '授权企业',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: StandaloneIotCard) => row.authorized_enterprise_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
|
||||
Reference in New Issue
Block a user