feat: 预计最终到期时间展示
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 8m8s

This commit is contained in:
luo
2026-07-22 16:31:01 +08:00
parent 8de4339505
commit 8faa2ea2ef
16 changed files with 342 additions and 0 deletions

View File

@@ -66,6 +66,9 @@ export interface AssetStartResponse {
// 资产实名认证策略
export type AssetRealnamePolicy = 'none' | 'before_order' | 'after_order'
// 预计最终到期时间状态
export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | string
// 批量更新资产实名认证策略请求
export interface BatchUpdateAssetRealnamePolicyRequest {
asset_ids: number[]
@@ -114,6 +117,10 @@ export interface AssetResolveResponse {
virtual_used_mb: number // 展示已用量
total_virtual_used_mb?: number | null // 所有套餐已用量(MB),未请求时为 null
total_virtual_remaining_mb?: number | null // 所有套餐剩余量(MB),未请求时为 null
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态
is_expiring?: boolean // 是否临期,由后端判断
reduction_pct: number // 展示增幅比例(小数,如 0.428571
device_protect_status?: DeviceProtectStatus // 保护期状态none / stop / start仅 device
activated_at: string // 激活时间

View File

@@ -399,6 +399,10 @@ export interface StandaloneIotCard {
data_usage_mb: number // 累计流量使用(MB)
current_month_usage_mb?: number // 自然月累计流量(MB)
current_month_start_date?: string | null // 本月开始日期
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
expiry_estimate_status?: string // 预计最终到期时间状态
is_expiring?: boolean // 是否临期,由后端判断
last_month_total_mb?: number // 上月流量总量(MB)
last_data_check_at?: string | null // 最后流量检查时间
last_real_name_check_at?: string | null // 最后实名检查时间

View File

@@ -67,6 +67,10 @@ export interface Device {
realname_policy?: string // 实名认证策略
real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名)
real_name_status_name: string // 实名状态名称
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
expiry_estimate_status?: string // 预计最终到期时间状态
is_expiring?: boolean // 是否临期,由后端判断
authorized_enterprise_id?: number | null // 当前有效授权企业ID
authorized_enterprise_name?: string | null // 当前有效授权企业名称
}

View File

@@ -0,0 +1,23 @@
import { formatDateTime } from './format'
export interface ExpiryEstimate {
estimated_final_expires_at?: string | null
days_until_final_expiry?: number | null
expiry_estimate_status?: string | null
is_expiring?: boolean
}
export const getExpiryEstimateText = (estimate: ExpiryEstimate): string => {
if (estimate.expiry_estimate_status === 'exact') {
return estimate.estimated_final_expires_at
? formatDateTime(estimate.estimated_final_expires_at)
: '-'
}
return estimate.expiry_estimate_status ? '待激活后起算' : '-'
}
export const getExpiryEstimateTooltip = (estimate: ExpiryEstimate): string => {
if (!estimate.is_expiring || estimate.days_until_final_expiry == null) return ''
return `剩余${estimate.days_until_final_expiry}`
}

View File

@@ -7,3 +7,4 @@ export * from './validate'
export * from './calculate'
export * from './voucher'
export * from './apiRateLimit'
export * from './expiryEstimate'

View File

@@ -86,6 +86,14 @@
<ElDescriptionsItem label="所有套餐剩余量">
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="预计套餐到期时间">
<span
:class="{ 'expiry-estimate--expiring': cardInfo?.is_expiring }"
:title="getExpiryEstimateTooltip(cardInfo)"
>
{{ getExpiryEstimateText(cardInfo) }}
</span>
</ElDescriptionsItem>
<template v-if="cardInfo?.bound_device_id">
<ElDescriptionsItem label="绑定设备号">
@@ -172,6 +180,14 @@
<ElDescriptionsItem label="所有套餐剩余量">
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="预计套餐到期时间">
<span
:class="{ 'expiry-estimate--expiring': cardInfo?.is_expiring }"
:title="getExpiryEstimateTooltip(cardInfo)"
>
{{ getExpiryEstimateText(cardInfo) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="最后同步时间">{{
formatDateTime(deviceRealtime?.last_sync_time) || '-'
}}</ElDescriptionsItem>
@@ -577,6 +593,7 @@
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
// Props
interface BindingCard {
@@ -624,6 +641,10 @@
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?: string
is_expiring?: boolean
last_sync_time?: string | null
last_data_check_at?: string | null
last_real_name_check_at?: string | null
@@ -965,6 +986,11 @@
word-break: break-all;
}
.expiry-estimate--expiring {
font-weight: 500;
color: var(--el-color-danger);
}
.card-header {
display: flex;
gap: 12px;

View File

@@ -140,6 +140,10 @@ export function useAssetInfo() {
gateway_extend: data.gateway_extend ?? '',
total_virtual_used_mb: data.total_virtual_used_mb ?? null,
total_virtual_remaining_mb: data.total_virtual_remaining_mb ?? null,
estimated_final_expires_at: data.estimated_final_expires_at ?? null,
days_until_final_expiry: data.days_until_final_expiry ?? null,
expiry_estimate_status: data.expiry_estimate_status,
is_expiring: data.is_expiring ?? false,
bound_device_id: data.bound_device_id,
bound_device_no: data.bound_device_no || '',
bound_device_name: data.bound_device_name || '',

View File

@@ -35,6 +35,10 @@ export interface AssetInfo {
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?: string
is_expiring?: boolean
last_sync_time?: string | null
last_data_check_at?: string | null
last_real_name_check_at?: string | null

View File

@@ -995,6 +995,7 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
import type { PackageSeriesResponse } from '@/types/api'
import type { EnterpriseItem } from '@/types/api/enterprise'
import type {
@@ -1606,6 +1607,7 @@
{ label: '设备名称', prop: 'device_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
{ label: '设备型号', prop: 'device_model' },
{ label: '设备类型', prop: 'device_type' },
{ label: '状态', prop: 'status' },
@@ -1890,6 +1892,20 @@
minWidth: 180,
showOverflowTooltip: true
},
{
prop: 'estimated_final_expires_at',
label: '预计套餐到期时间',
width: 180,
formatter: (row: Device) =>
h(
'span',
{
class: { 'expiry-estimate--expiring': row.is_expiring },
title: getExpiryEstimateTooltip(row)
},
getExpiryEstimateText(row)
)
},
{
prop: 'device_model',
label: '设备型号',
@@ -3201,4 +3217,9 @@
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.expiry-estimate--expiring {
font-weight: 500;
color: var(--el-color-danger);
}
</style>

View File

@@ -1045,6 +1045,7 @@
import { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { formatDateTime } from '@/utils/business/format'
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit'
import type {
StandaloneIotCard,
@@ -1946,6 +1947,7 @@
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '预计套餐到期时间', prop: 'estimated_final_expires_at' },
{ label: '自然月累计流量(MB)', prop: 'current_month_usage_mb' },
{ label: '本月开始日期', prop: 'current_month_start_date' },
{ label: '上月流量总量(MB)', prop: 'last_month_total_mb' },
@@ -2117,6 +2119,20 @@
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.series_name || '-'
},
{
prop: 'estimated_final_expires_at',
label: '预计套餐到期时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
h(
'span',
{
class: { 'expiry-estimate--expiring': row.is_expiring },
title: getExpiryEstimateTooltip(row)
},
getExpiryEstimateText(row)
)
},
{
prop: 'status',
label: '状态',
@@ -3027,4 +3043,9 @@
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.expiry-estimate--expiring {
font-weight: 500;
color: var(--el-color-danger);
}
</style>