462 lines
17 KiB
TypeScript
462 lines
17 KiB
TypeScript
/**
|
||
* 资产信息数据管理 Composable
|
||
* 负责资产详情数据的加载和状态管理
|
||
*/
|
||
|
||
import { ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { AssetService } from '@/api/modules'
|
||
import type {
|
||
AssetBoundCard,
|
||
AssetWalletResponse,
|
||
DeviceGatewayInfo,
|
||
AssetPackageUsageRecord
|
||
} from '@/types/api'
|
||
|
||
type TrafficDisplaySource = {
|
||
real_total_mb?: number | null
|
||
real_used_mb?: number | null
|
||
virtual_used_mb?: number | null
|
||
enable_virtual_data?: boolean | null
|
||
}
|
||
|
||
const normalizeOptionalText = (value?: string | null) =>
|
||
typeof value === 'string' ? value.trim() : ''
|
||
|
||
const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
||
'last_data_check_at',
|
||
'last_real_name_check_at',
|
||
'last_card_status_check_at'
|
||
])
|
||
|
||
/**
|
||
* 格式化数据大小(MB -> GB/MB)
|
||
*/
|
||
const formatDataSize = (mb: number) => {
|
||
if (mb >= 1024) {
|
||
return `${(mb / 1024).toFixed(2)}GB`
|
||
}
|
||
return `${mb.toFixed(2)}MB`
|
||
}
|
||
|
||
const getDisplayedTotalMb = (traffic?: TrafficDisplaySource | null) => traffic?.real_total_mb || 0
|
||
|
||
const getDisplayedUsedMb = (traffic?: TrafficDisplaySource | null) =>
|
||
traffic?.enable_virtual_data ? traffic?.virtual_used_mb || 0 : traffic?.real_used_mb || 0
|
||
|
||
const getDisplayedRemainingMb = (traffic?: TrafficDisplaySource | null) =>
|
||
getDisplayedTotalMb(traffic) - getDisplayedUsedMb(traffic)
|
||
|
||
const getDisplayedUsagePercentage = (traffic?: TrafficDisplaySource | null) => {
|
||
const totalMb = getDisplayedTotalMb(traffic)
|
||
const usedMb = getDisplayedUsedMb(traffic)
|
||
return totalMb > 0 ? `${((usedMb / totalMb) * 100).toFixed(2)}%` : '0.00%'
|
||
}
|
||
|
||
const mergeAssetBoundCards = (
|
||
existingCards: AssetBoundCard[] = [],
|
||
realtimeCards: AssetBoundCard[] = []
|
||
): AssetBoundCard[] =>
|
||
realtimeCards.map((realtimeCard) => {
|
||
const existingCard = existingCards.find((card) => card.iccid === realtimeCard.iccid)
|
||
if (!existingCard) return realtimeCard
|
||
|
||
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
||
for (const [key, value] of Object.entries(realtimeCard)) {
|
||
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
||
mergedCard[key] = value
|
||
}
|
||
}
|
||
return mergedCard as unknown as AssetBoundCard
|
||
})
|
||
|
||
export function useAssetInfo() {
|
||
// 状态管理
|
||
const loading = ref(false)
|
||
const cardInfo = ref<any>(null) // 使用 any 以保持与原始实现一致
|
||
const deviceRealtime = ref<DeviceGatewayInfo | null>(null)
|
||
const currentPackage = ref<AssetPackageUsageRecord | null>(null)
|
||
const currentPackageLoading = ref(false)
|
||
const currentPackageErrorMsg = ref('')
|
||
const packageList = ref<AssetPackageUsageRecord[]>([])
|
||
const walletInfo = ref<AssetWalletResponse | null>(null)
|
||
const walletLoading = ref(false)
|
||
const packageLoading = ref(false)
|
||
const realtimeLoading = ref(false)
|
||
|
||
/**
|
||
* 获取资产详情
|
||
* @param identifier - 资产标识符(ICCID、IMEI、SN、虚拟号或MSISDN)
|
||
*/
|
||
const fetchAssetDetail = async (identifier: string) => {
|
||
try {
|
||
loading.value = true
|
||
const response = await AssetService.resolveAsset(identifier)
|
||
|
||
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
|
||
|
||
// 映射API数据到页面显示格式
|
||
cardInfo.value = {
|
||
// 基本信息
|
||
asset_type: data.asset_type, // card 或 device
|
||
asset_id: data.asset_id,
|
||
identifier: assetIdentifier || '', // 资产标识符(卡用iccid,设备用virtual_no)
|
||
virtual_no: data.virtual_no,
|
||
status: data.status,
|
||
batch_no: data.batch_no,
|
||
shop_id: data.shop_id,
|
||
shop_name: data.shop_name,
|
||
series_id: data.series_id,
|
||
series_name: data.series_name,
|
||
real_name_status: data.real_name_status,
|
||
realname_policy: data.realname_policy,
|
||
real_name_at: data.real_name_at,
|
||
activation_status_name: data.activation_status_name || '',
|
||
activated_at: data.activated_at,
|
||
created_at: data.created_at,
|
||
updated_at: data.updated_at,
|
||
accumulated_recharge: data.accumulated_recharge,
|
||
first_commission_paid: data.first_commission_paid,
|
||
|
||
// 卡专属字段 (asset_type === 'card')
|
||
iccid: data.iccid || '',
|
||
msisdn: data.msisdn || '',
|
||
imsi: data.imsi || '',
|
||
carrier_id: data.carrier_id,
|
||
carrier_type: data.carrier_type || '',
|
||
carrier_name: data.carrier_name || '',
|
||
supplier: data.supplier || '',
|
||
network_status: data.network_status,
|
||
gateway_card_imei: resolvedGatewayCardImei,
|
||
resolved_gateway_card_imei: resolvedGatewayCardImei,
|
||
gateway_extend: data.gateway_extend ?? '',
|
||
bound_device_id: data.bound_device_id,
|
||
bound_device_no: data.bound_device_no || '',
|
||
bound_device_name: data.bound_device_name || '',
|
||
card_category: data.card_category || '',
|
||
activation_status: data.activation_status,
|
||
enable_polling: data.enable_polling,
|
||
|
||
// 设备专属字段 (asset_type === 'device')
|
||
device_name: data.device_name || '',
|
||
device_model: data.device_model || '',
|
||
device_type: data.device_type || '',
|
||
manufacturer: data.manufacturer || '',
|
||
imei: data.imei || '',
|
||
sn: data.sn || '',
|
||
max_sim_slots: data.max_sim_slots || 0,
|
||
bound_card_count: data.bound_card_count || 0,
|
||
cards: data.cards || [],
|
||
device_protect_status: data.device_protect_status,
|
||
online_status: data.online_status,
|
||
last_online_time: data.last_online_time,
|
||
software_version: data.software_version,
|
||
switch_mode: data.switch_mode,
|
||
last_gateway_sync_at: data.last_gateway_sync_at,
|
||
|
||
// 流量/套餐信息
|
||
current_package: data.current_package || '',
|
||
packageSeries: data.series_name || '-',
|
||
packageTotalFlow: formatDataSize(getDisplayedTotalMb(data)),
|
||
usedFlow: formatDataSize(getDisplayedUsedMb(data)),
|
||
remainFlow: formatDataSize(getDisplayedRemainingMb(data)),
|
||
usedFlowPercentage: getDisplayedUsagePercentage(data),
|
||
packageList: [] // 套餐列表需要单独调用API获取
|
||
}
|
||
|
||
// 加载其他数据
|
||
if (assetIdentifier) {
|
||
// 并行调用多个接口: 当前生效套餐、实时状态、钱包概况
|
||
// 套餐列表不在这里调用,由父组件调用以便管理分页状态
|
||
await Promise.all([
|
||
loadCurrentPackage(assetIdentifier),
|
||
loadRealtimeStatus(assetIdentifier, data.asset_type),
|
||
loadAssetWallet(assetIdentifier)
|
||
])
|
||
}
|
||
} else {
|
||
ElMessage.error(response.msg || '查询失败')
|
||
cardInfo.value = null
|
||
}
|
||
} catch (error: any) {
|
||
cardInfo.value = null
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载套餐列表
|
||
* @param identifier - 资产标识符
|
||
* @param page - 页码
|
||
* @param pageSize - 每页条数
|
||
*/
|
||
const loadPackageList = async (identifier: string, page: number = 1, pageSize: number = 10) => {
|
||
try {
|
||
packageLoading.value = true
|
||
const response = await AssetService.getAssetPackages(identifier, {
|
||
page,
|
||
page_size: pageSize
|
||
})
|
||
if (response.code === 0 && response.data) {
|
||
// 使用分页响应中的 items 数组
|
||
packageList.value = response.data.items || []
|
||
// 返回分页信息供外部使用
|
||
return {
|
||
items: response.data.items || [],
|
||
total: response.data.total || 0,
|
||
page: response.data.page || 1,
|
||
pageSize: response.data.page_size || pageSize
|
||
}
|
||
}
|
||
return { items: [], total: 0, page: 1, pageSize }
|
||
} catch (error) {
|
||
console.error('获取套餐列表失败:', error)
|
||
return { items: [], total: 0, page: 1, pageSize }
|
||
} finally {
|
||
packageLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载当前生效套餐
|
||
* @param identifier - 资产标识符
|
||
*/
|
||
const loadCurrentPackage = async (identifier: string) => {
|
||
try {
|
||
currentPackageLoading.value = true
|
||
currentPackageErrorMsg.value = ''
|
||
|
||
const response = await AssetService.getCurrentPackage(identifier)
|
||
|
||
if (response.code === 0) {
|
||
if (response.data) {
|
||
const pkg = response.data
|
||
|
||
currentPackage.value = {
|
||
id: pkg.package_usage_id || 0,
|
||
package_id: pkg.package_id || 0,
|
||
order_id: pkg.order_id || 0,
|
||
order_no: pkg.order_no || '',
|
||
package_name: pkg.package_name || '',
|
||
paid_amount: pkg.paid_amount || 0,
|
||
retail_amount: pkg.retail_amount || 0,
|
||
real_total_mb: pkg.real_total_mb || 0,
|
||
real_used_mb: pkg.real_used_mb || 0,
|
||
real_remaining_mb: (pkg.real_total_mb || 0) - (pkg.real_used_mb || 0),
|
||
virtual_total_mb: pkg.virtual_total_mb || 0,
|
||
virtual_used_mb: pkg.virtual_used_mb || 0,
|
||
virtual_remaining_mb: (pkg.virtual_total_mb || 0) - (pkg.virtual_used_mb || 0),
|
||
reduction_pct: pkg.reduction_pct || 0,
|
||
start_time: pkg.activated_at || '',
|
||
expire_time: pkg.expires_at || '',
|
||
expiry_base: pkg.expiry_base ?? null,
|
||
status: pkg.status || 0,
|
||
status_name: pkg.status_name,
|
||
duration_days: undefined,
|
||
package_type: pkg.package_type,
|
||
enable_virtual_data: pkg.enable_virtual_data,
|
||
created_at: pkg.created_at || ''
|
||
}
|
||
|
||
// 同时更新 cardInfo 中的流量显示信息(根据是否启用虚流量选择数据源)
|
||
cardInfo.value.current_package = pkg.package_name
|
||
cardInfo.value.packageTotalFlow = formatDataSize(getDisplayedTotalMb(pkg))
|
||
cardInfo.value.usedFlow = formatDataSize(getDisplayedUsedMb(pkg))
|
||
cardInfo.value.remainFlow = formatDataSize(getDisplayedRemainingMb(pkg))
|
||
cardInfo.value.usedFlowPercentage = getDisplayedUsagePercentage(pkg)
|
||
} else {
|
||
// code 为 0 但 data 为空,表示暂无当前生效套餐(正常情况)
|
||
currentPackage.value = null
|
||
currentPackageErrorMsg.value = ''
|
||
}
|
||
} else {
|
||
// 接口返回 code 不为 0,保存错误信息
|
||
currentPackage.value = null
|
||
currentPackageErrorMsg.value = response.msg || '获取当前套餐失败'
|
||
}
|
||
} catch (error: any) {
|
||
// 404表示无当前生效套餐,这是正常情况
|
||
currentPackage.value = null
|
||
if (error?.response?.status === 404) {
|
||
currentPackageErrorMsg.value = ''
|
||
} else {
|
||
console.error('获取当前套餐失败:', error)
|
||
currentPackageErrorMsg.value = error?.message || '获取当前套餐失败'
|
||
}
|
||
} finally {
|
||
currentPackageLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载实时状态
|
||
* @param identifier - 资产标识符
|
||
* @param assetType - 资产类型(card/device)
|
||
*/
|
||
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
|
||
try {
|
||
realtimeLoading.value = true
|
||
const response = await AssetService.getRealtimeStatus(identifier)
|
||
if (response.code === 0 && response.data) {
|
||
const data = response.data
|
||
// 更新实时状态数据
|
||
if (assetType === 'card') {
|
||
// 卡的实时状态
|
||
if (data.network_status !== undefined) {
|
||
cardInfo.value.network_status = data.network_status
|
||
}
|
||
if (data.real_name_status !== undefined) {
|
||
cardInfo.value.real_name_status = data.real_name_status
|
||
}
|
||
if ('gateway_card_imei' in data) {
|
||
const resolvedGatewayCardImei = normalizeOptionalText(
|
||
cardInfo.value?.resolved_gateway_card_imei
|
||
)
|
||
cardInfo.value.gateway_card_imei =
|
||
resolvedGatewayCardImei || normalizeOptionalText(data.gateway_card_imei)
|
||
}
|
||
if (data.last_sync_time !== undefined) {
|
||
cardInfo.value.last_sync_time = data.last_sync_time
|
||
}
|
||
if (data.last_data_check_at !== undefined) {
|
||
cardInfo.value.last_data_check_at = data.last_data_check_at
|
||
}
|
||
if (data.last_real_name_check_at !== undefined) {
|
||
cardInfo.value.last_real_name_check_at = data.last_real_name_check_at
|
||
}
|
||
if (data.last_card_status_check_at !== undefined) {
|
||
cardInfo.value.last_card_status_check_at = data.last_card_status_check_at
|
||
}
|
||
if (data.current_month_usage_mb !== undefined) {
|
||
cardInfo.value.current_month_usage_mb = data.current_month_usage_mb
|
||
}
|
||
if (data.last_gateway_reading_mb !== undefined) {
|
||
cardInfo.value.last_gateway_reading_mb = data.last_gateway_reading_mb
|
||
}
|
||
} else if (assetType === 'device') {
|
||
// 设备的实时状态
|
||
if (data.device_protect_status !== undefined) {
|
||
cardInfo.value.device_protect_status = data.device_protect_status
|
||
}
|
||
if (data.cards && data.cards.length > 0) {
|
||
// 合并实时卡数据与原始卡数据,保留 real_name_at 等静态字段。
|
||
// 设备详情里的实名状态、实名时间依赖选中的绑定卡,因此不能在实时刷新时丢掉这些字段。
|
||
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
||
}
|
||
if (data.last_data_check_at !== undefined) {
|
||
cardInfo.value.last_data_check_at = data.last_data_check_at
|
||
}
|
||
if (data.last_real_name_check_at !== undefined) {
|
||
cardInfo.value.last_real_name_check_at = data.last_real_name_check_at
|
||
}
|
||
if (data.last_card_status_check_at !== undefined) {
|
||
cardInfo.value.last_card_status_check_at = data.last_card_status_check_at
|
||
}
|
||
if (data.online_status !== undefined) {
|
||
cardInfo.value.online_status = data.online_status
|
||
}
|
||
if (data.last_online_time !== undefined) {
|
||
cardInfo.value.last_online_time = data.last_online_time
|
||
}
|
||
if (data.software_version !== undefined) {
|
||
cardInfo.value.software_version = data.software_version
|
||
}
|
||
if (data.switch_mode !== undefined) {
|
||
cardInfo.value.switch_mode = data.switch_mode
|
||
}
|
||
if (data.last_gateway_sync_at !== undefined) {
|
||
cardInfo.value.last_gateway_sync_at = data.last_gateway_sync_at
|
||
}
|
||
// 设备实时信息(Gateway 数据)
|
||
if (data.device_realtime !== undefined) {
|
||
deviceRealtime.value = data.device_realtime
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取实时状态失败:', error)
|
||
} finally {
|
||
realtimeLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载钱包概况
|
||
* @param identifier - 资产标识符
|
||
*/
|
||
const loadAssetWallet = async (identifier: string) => {
|
||
try {
|
||
walletLoading.value = true
|
||
const response = await AssetService.getAssetWallet(identifier)
|
||
if (response.code === 0 && response.data) {
|
||
walletInfo.value = response.data
|
||
}
|
||
} catch (error: any) {
|
||
if (error?.response?.status === 403) {
|
||
// 企业账号禁止查看钱包信息,不显示错误
|
||
walletInfo.value = null
|
||
} else {
|
||
console.error('获取钱包概况失败:', error)
|
||
}
|
||
} finally {
|
||
walletLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刷新资产数据(调用refresh接口后重新加载)
|
||
* @param identifier - 资产标识符
|
||
* @param assetType - 资产类型(card/device)
|
||
* @returns true if refresh was successful, false if rate limited (429)
|
||
*/
|
||
const refreshAsset = async (identifier: string, assetType: string): Promise<boolean> => {
|
||
try {
|
||
await AssetService.refreshAsset(identifier)
|
||
ElMessage.success('刷新成功')
|
||
|
||
// 刷新成功后重新加载实时状态数据
|
||
if (identifier) {
|
||
await loadRealtimeStatus(identifier, assetType)
|
||
}
|
||
return true
|
||
} catch (error: any) {
|
||
console.log(error)
|
||
// Check if it's a 429 rate limit error
|
||
if (error?.response?.status === 429) {
|
||
ElMessage.warning('操作过于频繁,请30秒后再试')
|
||
return false
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
|
||
return {
|
||
// State
|
||
loading,
|
||
cardInfo,
|
||
deviceRealtime,
|
||
currentPackage,
|
||
currentPackageLoading,
|
||
currentPackageErrorMsg,
|
||
packageList,
|
||
packageLoading,
|
||
walletInfo,
|
||
walletLoading,
|
||
realtimeLoading,
|
||
|
||
// Methods
|
||
fetchAssetDetail,
|
||
loadPackageList,
|
||
loadCurrentPackage,
|
||
loadRealtimeStatus,
|
||
loadAssetWallet,
|
||
refreshAsset
|
||
}
|
||
}
|