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

This commit is contained in:
luo
2026-07-24 09:31:11 +08:00
parent 127fdb560b
commit 15122e225a
13 changed files with 133 additions and 38 deletions

View File

@@ -15,7 +15,8 @@
## Decisions
- Decision: 统一使用资产详情和资产列表响应中的 `estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``is_expiring`,不调用额外计算接口。
- Decision: 统一使用以下五个后台响应字段:`estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``expiry_estimate_status_name``is_expiring`,不调用额外计算接口。
- Decision: `expiry_estimate_status` 仅使用 `exact``waiting_activation``none``invalid_data`。除 `exact` 外,`estimated_final_expires_at``days_until_final_expiry` 必须为 `null`;已过期的 `exact` 资产允许返回负数剩余天数。
- Decision: 仅当 `expiry_estimate_status=exact` 且存在 `estimated_final_expires_at` 时格式化展示日期;不可预计状态展示“待激活后起算”。
- Decision: `is_expiring=true` 是临期样式的唯一触发条件,`days_until_final_expiry` 只作为剩余天数展示或样式辅助信息,前端不自行判定临期阈值。
- Decision: 普通卡和设备列表保持既有服务端返回顺序与前端排序行为,不因临期字段重排。
@@ -37,4 +38,4 @@
## Open Questions
- `expiry_estimate_status``exact` 和待激活状态外的完整枚举及对应文案由后端接口文档确认;本提案要求未知状态不得伪造日期
- 后端若返回未约定的状态值,前端不得按 `estimated_final_expires_at` 是否为空展示日期,应按不可预计状态处理

View File

@@ -7,11 +7,13 @@
## What Changes
- 在资产详情、IoT 卡列表和设备列表增加统一的 `预计套餐到期时间` 展示。
- 资产详情接口和资产列表响应支持 `estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``is_expiring` 字段
- 资产详情接口和资产列表响应支持统一的 5 个字段:`estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``expiry_estimate_status_name``is_expiring`
- `expiry_estimate_status` 使用 `exact``waiting_activation``none``invalid_data` 四种状态;非 `exact` 状态下日期和剩余天数字段必须为 `null`
-`expiry_estimate_status=exact` 时,展示后端返回的 `estimated_final_expires_at`
- 当套餐尚待激活等无法预计最终日期时,展示“待激活后起算”,不得伪造日期。
-`is_expiring=true` 时,按后端返回的剩余天数使用临期颜色提示;普通资产列表不得因临期状态改变既有排序。
- 当前套餐自身的到期时间继续仅在套餐明细中展示;前端不得叠加套餐时长计算预计最终到期时间。
- 本次仅覆盖后台资产详情、IoT 卡列表和设备列表,不处理 C 端资产信息接口或页面。
## Impact
@@ -30,6 +32,9 @@
- `src/views/asset-management/device-list/index.vue`
- API contracts:
- `GET /api/admin/assets/resolve/{identifier}`
- IoT 卡和设备资产列表接口
- `GET /api/admin/iot-cards/standalone`
- `GET /api/admin/devices`
- Dependencies:
- 后端在资产详情与资产列表响应中返回预计最终到期字段。
- Out of scope:
- `GET /api/c/v1/asset/info` 及 C 端资产信息页面

View File

@@ -2,7 +2,7 @@
### Requirement: Asset Estimated Final Expiry Contract
The admin asset information integration SHALL preserve `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, and `is_expiring` returned by `GET /api/admin/assets/resolve/{identifier}`.
The admin asset information integration SHALL preserve the five backend fields `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, `expiry_estimate_status_name`, and `is_expiring` returned by `GET /api/admin/assets/resolve/{identifier}`. `estimated_final_expires_at` SHALL be an RFC3339 string or `null`, `days_until_final_expiry` SHALL be an integer or `null`, and `expiry_estimate_status` SHALL be one of `exact`, `waiting_activation`, `none`, or `invalid_data`. This requirement applies only to the admin frontend and excludes C-end asset information.
#### Scenario: Preserve an exact final expiry estimate
@@ -11,6 +11,15 @@ The admin asset information integration SHALL preserve `estimated_final_expires_
- **THEN** 前端状态 MUST preserve `estimated_final_expires_at` as `string | null`
- **AND** 前端状态 MUST preserve `days_until_final_expiry` as `number | null`
- **AND** 前端状态 MUST preserve `is_expiring` as a boolean
- **AND** 前端状态 MUST preserve `expiry_estimate_status_name` as the backend-provided status name
#### Scenario: Preserve null fields for non-exact estimates
- **GIVEN** 用户查询后台资产详情
- **WHEN** `expiry_estimate_status` is `waiting_activation`, `none`, or `invalid_data`
- **THEN** `estimated_final_expires_at` MUST be preserved as `null`
- **AND** `days_until_final_expiry` MUST be preserved as `null`
- **AND** 前端 MUST NOT calculate either field
#### Scenario: Preserve an unavailable final expiry estimate
@@ -18,6 +27,7 @@ The admin asset information integration SHALL preserve `estimated_final_expires_
- **WHEN** 接口返回待激活或其他不可预计的 `expiry_estimate_status`
- **THEN** 前端状态 MUST preserve `expiry_estimate_status`
- **AND** 前端 MUST NOT derive `estimated_final_expires_at` from current-package or package-detail fields
- **AND** 前端 MUST NOT use `expiry_estimate_status_name` as a substitute for the status enum when choosing the display rule
### Requirement: Asset Estimated Final Expiry Display
@@ -33,11 +43,18 @@ The admin asset information view SHALL display one asset-level field labeled `
#### Scenario: Display activation-pending final expiry
- **GIVEN** 资产详情返回等待激活等不可预计的 `expiry_estimate_status`
- **GIVEN** 资产详情返回 `expiry_estimate_status=waiting_activation`
- **WHEN** 页面渲染卡资产或设备资产基础信息
- **THEN** `预计套餐到期时间` MUST display `待激活后起算`
- **AND** 页面 MUST NOT display a fabricated date
#### Scenario: Handle an unknown estimate status safely
- **GIVEN** 资产详情返回未约定的 `expiry_estimate_status`
- **WHEN** 页面渲染资产基础信息
- **THEN** 页面 MUST treat the estimate as unavailable
- **AND** 页面 MUST NOT display `estimated_final_expires_at` as a date
#### Scenario: Display no-package placeholder
- **GIVEN** 资产没有当前或排队主套餐
@@ -59,3 +76,11 @@ The admin asset information view SHALL display one asset-level field labeled `
- **WHEN** 页面渲染单个套餐的到期时间
- **THEN** 页面 MUST continue to display that package's own expiry field in the package detail context
- **AND** 页面 MUST NOT replace it with `estimated_final_expires_at`
#### Scenario: Display no-package and invalid-data statuses
- **GIVEN** 资产详情返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **WHEN** 页面渲染资产基础信息
- **THEN** `none` MUST display a stable empty placeholder
- **AND** `invalid_data` MUST display `数据异常`
- **AND** 页面 MUST NOT display a fabricated date

View File

@@ -2,7 +2,7 @@
### Requirement: Device Estimated Final Expiry Display
The device management list integration SHALL preserve and display the backend device-level estimated final package expiry fields without traversing bound cards or calculating package continuation dates in the frontend.
The device management list integration SHALL preserve and display the five backend device-level estimated final package expiry fields `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, `expiry_estimate_status_name`, and `is_expiring` returned in `data.items[]` by `GET /api/admin/devices`. The status SHALL be one of `exact`, `waiting_activation`, `none`, or `invalid_data`; non-`exact` records SHALL have `estimated_final_expires_at=null` and `days_until_final_expiry=null`. The frontend SHALL NOT traverse bound cards or calculate package continuation dates. This requirement applies only to the admin device list.
#### Scenario: Display exact device final expiry estimate
@@ -13,11 +13,26 @@ The device management list integration SHALL preserve and display the backend de
#### Scenario: Display activation-pending device final expiry
- **GIVEN** 设备列表接口返回待激活等不可预计的 `expiry_estimate_status`
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=waiting_activation`
- **WHEN** 页面渲染设备列表行
- **THEN** `预计套餐到期时间` MUST display `待激活后起算`
- **AND** 页面 MUST NOT traverse bound cards or display a fabricated date
#### Scenario: Display device estimate status names for non-exact states
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **WHEN** 页面渲染设备列表行
- **THEN** `none` MUST display a stable empty placeholder
- **AND** `invalid_data` MUST display `数据异常`
- **AND** 页面 MUST use the status enum to choose the display rule
#### Scenario: Preserve null fields for non-exact device estimates
- **GIVEN** 设备列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **THEN** `estimated_final_expires_at` MUST be `null`
- **AND** `days_until_final_expiry` MUST be `null`
- **AND** 页面 MUST NOT derive either field from绑定卡或套餐数据
#### Scenario: Highlight expiring device without reordering
- **GIVEN** 设备列表接口返回 `is_expiring=true` and `days_until_final_expiry`
@@ -31,3 +46,9 @@ The device management list integration SHALL preserve and display the backend de
- **WHEN** 页面渲染预计套餐到期时间列
- **THEN** 页面 MUST display a stable placeholder
- **AND** 页面 MUST NOT use an individual package expiry as a substitute
#### Scenario: Preserve device status name without using it for business calculation
- **WHEN** 设备列表接口返回 `expiry_estimate_status_name`
- **THEN** 前端类型 MUST preserve the field
- **AND** 页面 MUST NOT calculate or replace the backend status name

View File

@@ -2,7 +2,7 @@
### Requirement: IoT Card Estimated Final Expiry Display
The IoT card management list integration SHALL preserve and display the backend asset-level estimated final package expiry fields without calculating package continuation dates in the frontend.
The IoT card management list integration SHALL preserve and display the five backend asset-level estimated final package expiry fields `estimated_final_expires_at`, `days_until_final_expiry`, `expiry_estimate_status`, `expiry_estimate_status_name`, and `is_expiring` returned in `data.items[]` by `GET /api/admin/iot-cards/standalone`. The status SHALL be one of `exact`, `waiting_activation`, `none`, or `invalid_data`; non-`exact` records SHALL have `estimated_final_expires_at=null` and `days_until_final_expiry=null`. The frontend SHALL NOT calculate package continuation dates. This requirement applies only to the admin IoT card list.
#### Scenario: Display exact card final expiry estimate
@@ -13,11 +13,26 @@ The IoT card management list integration SHALL preserve and display the backend
#### Scenario: Display activation-pending card final expiry
- **GIVEN** 卡列表接口返回待激活等不可预计的 `expiry_estimate_status`
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=waiting_activation`
- **WHEN** 页面渲染卡列表行
- **THEN** `预计套餐到期时间` MUST display `待激活后起算`
- **AND** 页面 MUST NOT calculate or display a fabricated date
#### Scenario: Display card estimate status names for non-exact states
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **WHEN** 页面渲染卡列表行
- **THEN** `none` MUST display a stable empty placeholder
- **AND** `invalid_data` MUST display `数据异常`
- **AND** 页面 MUST use the status enum to choose the display rule
#### Scenario: Preserve null fields for non-exact card estimates
- **GIVEN** 卡列表接口返回 `expiry_estimate_status=none``expiry_estimate_status=invalid_data`
- **THEN** `estimated_final_expires_at` MUST be `null`
- **AND** `days_until_final_expiry` MUST be `null`
- **AND** 页面 MUST NOT derive either field from套餐数据
#### Scenario: Highlight expiring card without reordering
- **GIVEN** 卡列表接口返回 `is_expiring=true` and `days_until_final_expiry`
@@ -31,3 +46,9 @@ The IoT card management list integration SHALL preserve and display the backend
- **WHEN** 页面渲染预计套餐到期时间列
- **THEN** 页面 MUST display a stable placeholder
- **AND** 页面 MUST NOT use an individual package expiry as a substitute
#### Scenario: Preserve card status name without using it for business calculation
- **WHEN** 卡列表接口返回 `expiry_estimate_status_name`
- **THEN** 前端类型 MUST preserve the field
- **AND** 页面 MUST NOT calculate or replace the backend status name

View File

@@ -1,25 +1,26 @@
## 1. API Contracts And Types
- [ ] 1.1 扩展资产详情响应和资产信息页面状态,支持 `estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``is_expiring`
- [ ] 1.2 扩展 IoT 卡和设备列表项类型,支持预计最终到期字段。
- [x] 1.1 扩展资产详情响应和资产信息页面状态,支持 5 个预计最终到期字段:`estimated_final_expires_at``days_until_final_expiry``expiry_estimate_status``expiry_estimate_status_name``is_expiring`
- [x] 1.2 扩展 IoT 卡和设备列表项类型,支持 5 个预计最终到期字段;不处理 C 端字段
- [x] 1.3 校验四种状态及字段约束:`exact` 可返回日期和剩余天数,其他状态的 `estimated_final_expires_at``days_until_final_expiry` 必须为 `null`
## 2. Asset Information
- [ ] 2.1 将资产解析接口返回的预计最终到期字段映射到资产详情页面状态。
- [ ] 2.2 在资产详情卡和设备基础信息中展示“预计套餐到期时间”。
- [ ] 2.3 `expiry_estimate_status=exact` 时格式化显示 `estimated_final_expires_at`;待激活等不可预计状态显示“待激活后起算”。
- [ ] 2.4 `is_expiring=true` 时按 `days_until_final_expiry` 应用临期样式,不根据日期或天数自行推导临期状态。
- [x] 2.1 将资产解析接口返回的预计最终到期字段映射到资产详情页面状态。
- [x] 2.2 在资产详情卡和设备基础信息中展示“预计套餐到期时间”。
- [x] 2.3 `expiry_estimate_status` 展示:`exact` 显示日期,`waiting_activation` 显示“待激活后起算”,`none` 显示占位,`invalid_data` 显示“数据异常”。
- [x] 2.4 `is_expiring=true` 时按 `days_until_final_expiry` 应用临期样式,不根据日期或天数自行推导临期状态。
## 3. Asset Lists
- [ ] 3.1 在 IoT 卡列表新增“预计套餐到期时间”可配置列,显示接口返回的预计日期或“待激活后起算”。
- [ ] 3.2 在设备列表新增“预计套餐到期时间”可配置列,显示接口返回的预计日期或“待激活后起算”。
- [ ] 3.3 在两个列表对 `is_expiring=true` 的预计到期时间使用临期样式,且不改变现有列表排序。
- [x] 3.1 在 IoT 卡列表新增“预计套餐到期时间”可配置列,显示接口返回的预计日期或“待激活后起算”。
- [x] 3.2 在设备列表新增“预计套餐到期时间”可配置列,显示接口返回的预计日期或“待激活后起算”。
- [x] 3.3 在两个列表对 `is_expiring=true` 的预计到期时间使用临期样式,且不改变现有列表排序。
## 4. Verification
- [ ] 4.1 验证无套餐时不伪造预计日期并显示稳定占位内容。
- [ ] 4.2 验证仅当前套餐和存在多个排队主套餐时,资产详情和两个列表均显示后端 `estimated_final_expires_at`
- [ ] 4.3 验证等待激活等不可预计状态显示“待激活后起算”,不显示计算出的日期。
- [ ] 4.4 验证 `is_expiring=true` 时使用临期样式,且卡、设备列表顺序不变。
- [ ] 4.5 运行相关类型检查、lint 或构建验证。
- [x] 4.1 验证无套餐时不伪造预计日期并显示稳定占位内容。
- [x] 4.2 验证仅当前套餐和存在多个排队主套餐时,资产详情和两个列表均显示后端 `estimated_final_expires_at`
- [x] 4.3 验证等待激活等不可预计状态显示“待激活后起算”,不显示计算出的日期。
- [x] 4.4 验证 `is_expiring=true` 时使用临期样式,且卡、设备列表顺序不变。
- [x] 4.5 运行相关类型检查、lint 或构建验证。

View File

@@ -67,7 +67,7 @@ export interface AssetStartResponse {
export type AssetRealnamePolicy = 'none' | 'before_order' | 'after_order'
// 预计最终到期时间状态
export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | string
export type ExpiryEstimateStatus = 'exact' | 'waiting_activation' | 'none' | 'invalid_data'
// 批量更新资产实名认证策略请求
export interface BatchUpdateAssetRealnamePolicyRequest {
@@ -120,6 +120,7 @@ export interface AssetResolveResponse {
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 // 是否临期,由后端判断
reduction_pct: number // 展示增幅比例(小数,如 0.428571
device_protect_status?: DeviceProtectStatus // 保护期状态none / stop / start仅 device

View File

@@ -3,6 +3,7 @@
*/
import { PaginationParams, ImportTask } from './common'
import type { ExpiryEstimateStatus } from './asset'
// 运营商类型
export enum Operator {
@@ -401,7 +402,8 @@ export interface StandaloneIotCard {
current_month_start_date?: string | null // 本月开始日期
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
expiry_estimate_status?: string // 预计最终到期时间状态
expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态
expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称
is_expiring?: boolean // 是否临期,由后端判断
last_month_total_mb?: number // 上月流量总量(MB)
last_data_check_at?: string | null // 最后流量检查时间

View File

@@ -3,6 +3,7 @@
*/
import { PaginationParams } from './common'
import type { ExpiryEstimateStatus } from './asset'
// ========== 设备状态枚举 ==========
@@ -69,7 +70,8 @@ export interface Device {
real_name_status_name: string // 实名状态名称
estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间
days_until_final_expiry?: number | null // 距预计最终到期的剩余天数
expiry_estimate_status?: string // 预计最终到期时间状态
expiry_estimate_status?: ExpiryEstimateStatus // 预计最终到期时间状态
expiry_estimate_status_name?: string | null // 预计最终到期时间状态名称
is_expiring?: boolean // 是否临期,由后端判断
authorized_enterprise_id?: number | null // 当前有效授权企业ID
authorized_enterprise_name?: string | null // 当前有效授权企业名称

View File

@@ -1,20 +1,28 @@
import { formatDateTime } from './format'
import type { ExpiryEstimateStatus } from '@/types/api'
export interface ExpiryEstimate {
estimated_final_expires_at?: string | null
days_until_final_expiry?: number | null
expiry_estimate_status?: string | null
expiry_estimate_status?: ExpiryEstimateStatus | null
expiry_estimate_status_name?: string | null
is_expiring?: boolean
}
export const getExpiryEstimateText = (estimate: ExpiryEstimate): string => {
if (estimate.expiry_estimate_status === 'exact') {
switch (estimate.expiry_estimate_status) {
case 'exact':
return estimate.estimated_final_expires_at
? formatDateTime(estimate.estimated_final_expires_at)
: '-'
case 'waiting_activation':
return '待激活后起算'
case 'invalid_data':
return '数据异常'
case 'none':
default:
return '-'
}
return estimate.expiry_estimate_status ? '待激活后起算' : '-'
}
export const getExpiryEstimateTooltip = (estimate: ExpiryEstimate): string => {

View File

@@ -588,7 +588,12 @@
ElMessage
} from 'element-plus'
import { CopyDocument } from '@element-plus/icons-vue'
import type { AssetExchangeTrace, AssetExchangeTraceAsset, AssetPollingStatus } from '@/types/api'
import type {
AssetExchangeTrace,
AssetExchangeTraceAsset,
AssetPollingStatus,
ExpiryEstimateStatus
} from '@/types/api'
import { useAssetFormatters } from '../composables/useAssetFormatters'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
@@ -643,7 +648,8 @@
total_virtual_remaining_mb?: number | null
estimated_final_expires_at?: string | null
days_until_final_expiry?: number | null
expiry_estimate_status?: string
expiry_estimate_status?: ExpiryEstimateStatus
expiry_estimate_status_name?: string | null
is_expiring?: boolean
last_sync_time?: string | null
last_data_check_at?: string | null

View File

@@ -143,6 +143,7 @@ export function useAssetInfo() {
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,
expiry_estimate_status_name: data.expiry_estimate_status_name ?? null,
is_expiring: data.is_expiring ?? false,
bound_device_id: data.bound_device_id,
bound_device_no: data.bound_device_no || '',

View File

@@ -2,7 +2,7 @@
* 资产信息页面类型定义
*/
import type { AssetExchangeTrace, AssetPollingStatus } from '@/types/api'
import type { AssetExchangeTrace, AssetPollingStatus, ExpiryEstimateStatus } from '@/types/api'
// 资产类型枚举
export type AssetType = 'card' | 'device'
@@ -37,7 +37,8 @@ export interface AssetInfo {
total_virtual_remaining_mb?: number | null
estimated_final_expires_at?: string | null
days_until_final_expiry?: number | null
expiry_estimate_status?: string
expiry_estimate_status?: ExpiryEstimateStatus
expiry_estimate_status_name?: string | null
is_expiring?: boolean
last_sync_time?: string | null
last_data_check_at?: string | null