fix:进度条
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m44s

This commit is contained in:
sexygoat
2026-04-30 17:01:00 +08:00
parent 78a41513bd
commit 81155fd8e4
12 changed files with 457 additions and 178 deletions

View File

@@ -132,6 +132,8 @@ export interface DeviceGatewayInfo {
connect_time?: string // 本次联网时长(秒)
last_online_time?: string // 设备最后在线时间
last_update_time?: string // 信息最后更新时间
signal_quality?: string // 信号综合判断
signal_bad_reason?: string // 信号较弱原因提示
rsrp?: number // 信号接收功率(dBm)
rsrq?: number // 信号接收质量(dB)
rssi?: string // 信号强度
@@ -145,6 +147,7 @@ export interface DeviceGatewayInfo {
mac_address?: string // MAC 地址
limit_speed?: number // 限速(KB/s)0=不限
current_iccid?: string // 当前使用卡 ICCID
client_number?: number // 当前已连接客户端数
max_clients?: number // 最大连接客户端数
software_version?: string // 固件版本
switch_mode?: string // 切卡模式
@@ -199,6 +202,7 @@ export interface AssetPackageUsageRecord {
package_usage_id?: number // 套餐使用记录 ID
package_id?: number // 套餐 ID
package_name?: string // 套餐名称
order_no?: string // 订单号
package_type?: PackageType // formal正式套餐/ addon加油包
usage_type?: UsageType // 使用类型single_card/device
status?: PackageUsageStatus // 0 待生效 / 1 生效中 / 2 已用完 / 3 已过期 / 4 已失效

View File

@@ -114,11 +114,6 @@
<ElDescriptionsItem label="切卡模式">{{
getSwitchModeName(cardInfo?.switch_mode)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="信号强度">
<ElTag :type="getSignalStrengthType()" size="small">
{{ calculateSignalStrength() }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="已绑定卡数">{{
cardInfo?.bound_card_count || 0
}}</ElDescriptionsItem>
@@ -266,44 +261,16 @@
</ElDescriptionsItem>
<!-- 信号信息 -->
<ElDescriptionsItem label="信号强度(RSSI)">
{{
deviceRealtime.rssi !== undefined &&
deviceRealtime.rssi !== null &&
deviceRealtime.rssi !== ''
? deviceRealtime.rssi
: '-'
}}
<ElDescriptionsItem label="信号综合判断">
<ElTag :type="getSignalQualityType(deviceRealtime.signal_quality)" size="small">
{{ getSignalQualityText(deviceRealtime.signal_quality) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="接收功率(RSRP)">
{{
deviceRealtime.rsrp !== undefined &&
deviceRealtime.rsrp !== null &&
deviceRealtime.rsrp !== ''
? `${deviceRealtime.rsrp} dBm`
: '-'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="接收质量(RSRQ)">
{{
deviceRealtime.rsrq !== undefined &&
deviceRealtime.rsrq !== null &&
deviceRealtime.rsrq !== ''
? `${deviceRealtime.rsrq} dB`
: '-'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="信噪比(SINR)">
{{
deviceRealtime.sinr !== undefined &&
deviceRealtime.sinr !== null &&
deviceRealtime.sinr !== ''
? `${deviceRealtime.sinr} dB`
: '-'
}}
<ElDescriptionsItem label="信号较弱原因" :span="3">
{{ getSignalBadReasonText(deviceRealtime.signal_bad_reason) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="已连接设备数">
{{ deviceRealtime.client_number }} / {{ deviceRealtime.max_clients }}
{{ deviceRealtime.client_number ?? '-' }} / {{ deviceRealtime.max_clients ?? '-' }}
</ElDescriptionsItem>
<!-- WiFi 信息 -->
@@ -437,7 +404,7 @@
</template>
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { computed } from 'vue'
import {
ElCard,
ElDescriptions,
@@ -479,6 +446,7 @@
msisdn?: string
carrier_name?: string
real_name_status?: number
realname_policy?: string
network_status?: number
current_month_usage_mb?: number
last_gateway_reading_mb?: number
@@ -510,16 +478,15 @@
interface DeviceRealtimeInfo {
online_status?: number
status?: number
battery_level?: number
battery_level?: number | null
run_time?: string
connect_time?: string
last_online_time?: string
rssi?: string
rsrp?: string
rsrq?: string
sinr?: string
client_number?: number
max_clients?: number
current_iccid?: string
signal_quality?: string
signal_bad_reason?: string
wifi_enabled?: boolean
ssid?: string
wifi_password?: string
@@ -590,7 +557,6 @@
const emit = defineEmits<Emits>()
// 使用格式化工具
const deviceRealtimeRef = toRef(props, 'deviceRealtime')
const {
formatDataSize,
formatDuration,
@@ -601,10 +567,11 @@
getAssetStatusType,
getOnlineStatusName,
getOnlineStatusType,
calculateSignalStrength,
getSignalStrengthType,
getSignalQualityText,
getSignalQualityType,
getSignalBadReasonText,
getSwitchModeName
} = useAssetFormatters(deviceRealtimeRef)
} = useAssetFormatters()
// 排序后的设备卡列表(按卡槽位置排序,当前卡排在最前面)
const sortedDeviceCards = computed(() => {
@@ -628,8 +595,8 @@
})
// 处理轮询开关变化
const handlePollingChange = (value: boolean) => {
emit('update:pollingEnabled', value)
const handlePollingChange = (value: string | number | boolean) => {
emit('update:pollingEnabled', Boolean(value))
}
// 跳转到卡片信息

View File

@@ -96,28 +96,46 @@
</span>
</div>
<div class="progress-wrapper">
<ElProgress
:percentage="
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.virtual_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
<div class="progress-track">
<ElProgress
:show-text="false"
:percentage="
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.virtual_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.virtual_total_mb || 0
)
)
"
:stroke-width="10"
/>
<div
class="breakpoint-marker"
:style="
getBreakpointMarkerStyle(
currentPackage.virtual_total_mb || 0,
currentPackage.real_total_mb || 0
)
"
:title="`停机阈值/真流量总量: ${formatDataSize(currentPackage.virtual_total_mb || 0)} / ${formatDataSize(currentPackage.real_total_mb || 0)}`"
></div>
</div>
<span class="progress-percentage">
{{
formatProgressPercentage(
getUsageProgress(
currentPackage.virtual_used_mb || 0,
currentPackage.virtual_total_mb || 0
)
)
"
:stroke-width="10"
/>
<div
class="breakpoint-marker"
:style="{ left: getBreakpointPosition(currentPackage) + '%' }"
:title="`停机阈值/真流量总量: ${formatDataSize(currentPackage.virtual_total_mb || 0)} / ${formatDataSize(currentPackage.real_total_mb || 0)}`"
></div>
}}
</span>
</div>
</div>
</div>
@@ -231,8 +249,14 @@
import { formatDateTime } from '@/utils/business/format'
import type { PackageInfo } from '../types'
const { formatAmount, formatDataSize, getUsageProgress, getProgressColorByPercentage } =
useAssetFormatters()
const {
formatAmount,
formatDataSize,
getUsageProgress,
getProgressColorByPercentage,
formatProgressPercentage,
getBreakpointMarkerStyle
} = useAssetFormatters()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
@@ -269,11 +293,6 @@
}
return typeMap[type || ''] || '-'
}
const getBreakpointPosition = (pkg: PackageInfo) => {
if (!pkg.real_total_mb || pkg.real_total_mb === 0) return 0
return (pkg.virtual_total_mb / pkg.real_total_mb) * 100
}
</script>
<style scoped lang="scss">
@@ -369,18 +388,34 @@
}
.progress-wrapper {
position: relative;
display: flex;
align-items: center;
gap: 1px;
.breakpoint-marker {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background-color: var(--el-color-danger);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
.progress-track {
position: relative;
flex: 1;
min-width: 0;
.breakpoint-marker {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background-color: var(--el-color-danger);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
}
}
.progress-percentage {
min-width: 44px;
font-size: 14px;
color: var(--el-text-color-secondary);
text-align: right;
white-space: nowrap;
}
}
}

View File

@@ -169,6 +169,8 @@
{ label: '设备重置', value: 'device_reset' },
{ label: '设备分配', value: 'device_allocate' },
{ label: '设备回收', value: 'device_recall' },
{ label: '设备绑卡', value: 'device_bind_card' },
{ label: '设备解绑卡', value: 'device_unbind_card' },
{ label: '卡分配', value: 'card_allocate' },
{ label: '卡回收', value: 'card_recall' },
{ label: '卡停机', value: 'card_stop' },
@@ -195,6 +197,8 @@
device_reset: 'warning',
device_allocate: 'primary',
device_recall: 'warning',
device_bind_card: 'primary',
device_unbind_card: 'warning',
card_allocate: 'primary',
card_recall: 'warning',
card_stop: 'danger',
@@ -223,17 +227,26 @@
asset_type: '资产类型',
batch_no: '批次号',
card_category: '卡类型',
device_imei: '设备IMEI',
device_sn: '设备SN',
device_virtual_no: '设备虚拟号',
device_virtual_nos: '设备虚拟号',
enable_polling: '轮询开关',
file_key: '文件存储键',
first_realname_at: '首次实名时间',
iccid: 'ICCID',
iccids: 'ICCID',
network_status: '网络状态',
real_name_status: '实名状态',
realname_policy: '实名认证策略',
shop_name: '店铺',
source_service: '来源服务',
source_shop_name: '来源店铺',
status: '资产状态',
stop_reason: '停机原因',
switch_mode: '切卡模式'
switch_mode: '切卡模式',
target_shop_name: '目标店铺',
to_shop_name: '目标店铺'
}
const realnamePolicyMap: Record<string, string> = {
@@ -371,6 +384,13 @@
if (typeof value === 'boolean') return value ? '是' : '否'
if (typeof value === 'number') return String(value)
if (typeof value === 'string') return value || '空'
if (Array.isArray(value)) {
if (value.length === 0) return '空'
if (value.every((item) => ['string', 'number', 'boolean'].includes(typeof item))) {
return value.map((item) => formatValue(item)).join('、')
}
return JSON.stringify(value)
}
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}

View File

@@ -4,7 +4,10 @@
<div class="card-header">
<span>套餐列表</span>
<div class="card-header-right">
<ElButton type="primary" :disabled="props.boundDeviceId" @click="handleShowRecharge"
<ElButton
type="primary"
:disabled="Boolean(props.boundDeviceId)"
@click="handleShowRecharge"
>套餐充值</ElButton
>
</div>
@@ -97,28 +100,46 @@
>
</div>
<div class="progress-wrapper">
<ElProgress
style="margin-top: 4px"
:percentage="
getUsageProgress(
scope.row.virtual_used_mb || 0,
scope.row.virtual_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
<div class="progress-track">
<ElProgress
style="margin-top: 4px"
:show-text="false"
:percentage="
getUsageProgress(
scope.row.virtual_used_mb || 0,
scope.row.virtual_total_mb || 0
)
"
:color="
getProgressColorByPercentage(
getUsageProgress(
scope.row.virtual_used_mb || 0,
scope.row.virtual_total_mb || 0
)
)
"
/>
<div
class="breakpoint-marker"
:style="
getBreakpointMarkerStyle(
scope.row.virtual_total_mb || 0,
scope.row.real_total_mb || 0
)
"
:title="`停机阈值/真流量总量: ${formatDataSize(scope.row.virtual_total_mb || 0)} / ${formatDataSize(scope.row.real_total_mb || 0)}`"
></div>
</div>
<span class="progress-percentage">
{{
formatProgressPercentage(
getUsageProgress(
scope.row.virtual_used_mb || 0,
scope.row.virtual_total_mb || 0
)
)
"
/>
<div
class="breakpoint-marker"
:style="{ left: getBreakpointPosition(scope.row) + '%' }"
:title="`停机阈值/真流量总量: ${formatDataSize(scope.row.virtual_total_mb || 0)} / ${formatDataSize(scope.row.real_total_mb || 0)}`"
></div>
}}
</span>
</div>
</template>
<template v-else>
@@ -346,14 +367,11 @@
formatAmount,
getPackageStatusTagType,
getUsageProgress,
getProgressColorByPercentage
getProgressColorByPercentage,
formatProgressPercentage,
getBreakpointMarkerStyle
} = useAssetFormatters()
const getBreakpointPosition = (pkg: PackageInfo) => {
if (!pkg.real_total_mb || pkg.real_total_mb === 0) return 0
return (pkg.virtual_total_mb / pkg.real_total_mb) * 100
}
// 套餐名称点击
const handlePackageNameClick = (packageRow: PackageInfo) => {
if (!hasAuth('package_usage:daily_records')) {
@@ -459,18 +477,34 @@
}
.progress-wrapper {
position: relative;
display: flex;
align-items: center;
gap: 1px;
.breakpoint-marker {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background-color: var(--el-color-danger);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
.progress-track {
position: relative;
flex: 1;
min-width: 0;
.breakpoint-marker {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
background-color: var(--el-color-danger);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
}
}
.progress-percentage {
min-width: 44px;
font-size: 14px;
color: var(--el-text-color-secondary);
text-align: right;
white-space: nowrap;
}
}
}

View File

@@ -2,11 +2,9 @@
* 资产信息格式化工具函数
*/
import type { Ref } from 'vue'
type TagType = 'primary' | 'success' | 'warning' | 'info' | 'danger'
export function useAssetFormatters(deviceRealtime?: Ref<any>) {
export function useAssetFormatters() {
// 格式化数据大小MB 转 GB/MB
const formatDataSize = (mb: number) => {
if (mb >= 1024) {
@@ -145,57 +143,27 @@ export function useAssetFormatters(deviceRealtime?: Ref<any>) {
return map[status] || 'info'
}
const parseSignalValue = (value: unknown) => {
if (value === null || value === undefined || value === '') return null
const parsed = Number(value)
return Number.isNaN(parsed) ? null : parsed
// 获取设备信号综合判断文案
const getSignalQualityText = (quality?: string | null) => {
return quality?.trim() || '暂无数据'
}
const clamp = (value: number, min: number, max: number) => {
return Math.min(Math.max(value, min), max)
}
const calculateSignalScore = () => {
if (!deviceRealtime?.value) return null
const rsrp = parseSignalValue(deviceRealtime.value.rsrp)
const rsrq = parseSignalValue(deviceRealtime.value.rsrq)
const sinr = parseSignalValue(deviceRealtime.value.sinr)
// RSRP、RSRQ 必须是负数SINR 允许 0 或负值
if (rsrp === null || rsrq === null || sinr === null) return null
if (rsrp >= 0 || rsrq >= 0) return null
const rsrpScore = clamp((rsrp + 120) * 2, 0, 100)
const rsrqScore = clamp((rsrq + 20) * 5, 0, 100)
const sinrScore = clamp(sinr * 5, 0, 100)
return rsrpScore * 0.5 + sinrScore * 0.3 + rsrqScore * 0.2
}
// 计算信号强度等级:基于 RSRP、RSRQ、SINR 的综合评分RSSI 不参与
const calculateSignalStrength = () => {
const score = calculateSignalScore()
if (score === null) return '-'
if (score >= 85) return '极好'
if (score >= 70) return '良好'
if (score >= 50) return '一般'
if (score >= 30) return '较差'
return '很差'
}
// 获取信号强度标签类型
const getSignalStrengthType = (): TagType => {
const strength = calculateSignalStrength()
// 获取设备信号综合判断标签类型
const getSignalQualityType = (quality?: string | null): TagType => {
const normalizedQuality = getSignalQualityText(quality)
const map: Record<string, TagType> = {
: 'success',
: 'success',
: 'warning',
: 'danger',
: 'danger'
: 'success',
: 'success',
: 'warning',
: 'danger',
: 'info'
}
return map[strength] || 'info'
return map[normalizedQuality] || 'info'
}
// 获取设备弱信号原因文案
const getSignalBadReasonText = (reason?: string | null) => {
return reason?.trim() || '暂时无法判断'
}
// 获取切卡模式名称
@@ -242,6 +210,31 @@ export function useAssetFormatters(deviceRealtime?: Ref<any>) {
return '#f56c6c'
}
// 格式化进度百分比文本
const formatProgressPercentage = (percentage: number): string => {
if (!Number.isFinite(percentage)) return '0%'
const normalizedPercentage = Math.min(Math.max(percentage, 0), 100)
if (Number.isInteger(normalizedPercentage)) return `${normalizedPercentage}%`
return `${normalizedPercentage.toFixed(2).replace(/\.?0+$/, '')}%`
}
// 计算断点位置百分比(用于虚流量停机阈值标记)
const getBreakpointPercentage = (breakpoint: number, total: number): number => {
if (!total || total <= 0) return 0
const percentage = (breakpoint / total) * 100
return Math.min(Math.max(Math.round(percentage * 100) / 100, 0), 100)
}
// 生成断点标记样式,避免圆点在进度条两端溢出
const getBreakpointMarkerStyle = (breakpoint: number, total: number, markerSize: number = 8) => {
const percentage = getBreakpointPercentage(breakpoint, total)
const halfMarkerSize = markerSize / 2
return {
left: `clamp(${halfMarkerSize}px, ${percentage}%, calc(100% - ${halfMarkerSize}px))`
}
}
return {
formatDataSize,
formatAmount,
@@ -256,12 +249,16 @@ export function useAssetFormatters(deviceRealtime?: Ref<any>) {
getOnlineStatusType,
getWalletStatusType,
getPackageStatusTagType,
calculateSignalStrength,
getSignalStrengthType,
getSignalQualityText,
getSignalQualityType,
getSignalBadReasonText,
getSwitchModeName,
getTransactionTypeTag,
getPaymentStatusType,
getUsageProgress,
getProgressColorByPercentage
getProgressColorByPercentage,
formatProgressPercentage,
getBreakpointPercentage,
getBreakpointMarkerStyle
}
}

View File

@@ -46,33 +46,50 @@ export interface BindingCard {
// 设备实时信息接口
export interface DeviceRealtimeInfo {
device_id?: number
device_id?: number | string
status?: string
online_status?: number
report_time?: string
boot_time?: string
running_time?: string
run_time?: string
connect_time?: string
last_online_time?: string
battery_level?: number | null
client_number?: number
max_clients?: number
rssi?: string
rsrp?: string
rsrq?: string
sinr?: string
signal_quality?: string
signal_bad_reason?: string
band?: string
pci?: string
wifi_enabled?: number
wifi_enabled?: boolean | number
ssid?: string
wifi_ssid?: string
wifi_password?: string
network_type?: string
ip_address?: string
wan_ip?: string
lan_ip?: string
mac_address?: string
apn?: string
current_iccid?: string
imsi?: string
uplink_rate?: number
downlink_rate?: number
uplink_speed_limit?: number
downlink_speed_limit?: number
limit_speed?: number
switch_mode?: string
imei?: string
model?: string
software_version?: string
firmware_version?: string
sync_interval?: number
last_update_time?: string
}
// 套餐信息接口
@@ -81,6 +98,7 @@ export interface PackageInfo {
package_id: number
package_name: string
package_price: number
paid_amount?: number
real_total_mb: number
real_used_mb: number
real_remaining_mb: number
@@ -96,6 +114,7 @@ export interface PackageInfo {
package_type?: string // 套餐类型: formal正式套餐/ addon加油包
enable_virtual_data?: boolean // 是否启用虚流量
order_no?: string // 订单号
created_at?: string // 创建时间
}
// 钱包信息接口 (使用已有的 AssetWalletResponse)