feat: 新增运营商停机原因
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m44s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m44s
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Change: 补充资产详情 gateway_extend 卡状态扩展展示
|
||||
|
||||
## Why
|
||||
|
||||
当前后台“资产信息”页还没有消费接口新增的 `gateway_extend` 字段,因此运营人员无法在资产详情中直接看到运营商侧实际停机原因。
|
||||
|
||||
最新接口契约已经补充两类返回:
|
||||
- 设备资产的绑定卡列表项新增 `gateway_extend`
|
||||
- 卡资产详情新增 `gateway_extend`
|
||||
|
||||
如果前端不补齐这部分契约和展示规范,资产详情页会继续丢失上游已经返回的停机原因信息,影响停机排查和运营判断。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 为后台“资产信息”页新增 `gateway_extend` 消费规范:
|
||||
- 卡资产详情需接收并保留 `gateway_extend`
|
||||
- 设备资产绑定卡列表项需接收并保留 `gateway_extend`
|
||||
- 页面展示时需按上游原样展示该字段,不做二次翻译或改写
|
||||
- 为后台“资产信息”页新增 `gateway_extend` 展示规范:
|
||||
- 卡资产在“卡信息”区域展示运营商侧实际停机原因
|
||||
- 设备资产在“绑定卡列表”中展示每张卡的运营商侧实际停机原因
|
||||
- 字段为空时展示稳定占位内容
|
||||
- 范围约束:
|
||||
- 仅覆盖后台管理端资产详情页
|
||||
- 不要求本次同步修改卡列表、设备列表或 C 端页面
|
||||
- 不改变后端接口路径、方法和字段命名
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `asset-information`
|
||||
- Affected code:
|
||||
- `src/types/api/asset.ts`
|
||||
- `src/views/asset-management/asset-information/composables/useAssetInfo.ts`
|
||||
- `src/views/asset-management/asset-information/components/BasicInfoCard.vue`
|
||||
- `src/views/asset-management/asset-information/types.ts`
|
||||
- Dependencies:
|
||||
- 依赖后端资产详情相关响应已返回 `gateway_extend`
|
||||
- Breaking changes:
|
||||
- 无
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Admin Asset Gateway Extend Contract
|
||||
|
||||
The admin asset information integration SHALL accept and preserve `gateway_extend` for card asset details and device bound-card entries.
|
||||
|
||||
#### Scenario: Card asset detail preserves gateway_extend
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** 资产详情响应返回 `gateway_extend`
|
||||
- **WHEN** 前端解析并映射卡资产详情数据
|
||||
- **THEN** 页面状态 MUST 保留 `gateway_extend` 原始字符串
|
||||
- **AND** 后续页面渲染 MUST 可以直接消费该字段
|
||||
|
||||
#### Scenario: Device bound-card entries preserve gateway_extend
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的设备资产详情
|
||||
- **AND** 绑定卡列表数据中的某条记录返回 `gateway_extend`
|
||||
- **WHEN** 前端解析初次详情响应或后续刷新返回的绑定卡数据
|
||||
- **THEN** 对应卡记录 MUST 保留各自的 `gateway_extend` 原始字符串
|
||||
- **AND** 绑定卡列表的刷新合并过程 MUST NOT 无故丢失已有的 `gateway_extend`
|
||||
|
||||
### Requirement: Admin Asset Gateway Extend Display
|
||||
|
||||
The admin asset information view SHALL display `gateway_extend` as the carrier-side actual stop reason for card assets and device bound cards.
|
||||
|
||||
#### Scenario: Show gateway_extend on card asset page
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **WHEN** 页面渲染“卡信息”区域
|
||||
- **THEN** 页面 MUST 提供“运营商停机原因”或等效标签来展示 `gateway_extend`
|
||||
- **AND** 展示内容 MUST 与上游返回的 `gateway_extend` 保持一致
|
||||
- **AND** 当 `gateway_extend` 为空时,页面 MUST 显示稳定占位内容
|
||||
|
||||
#### Scenario: Show gateway_extend in device bound-card list
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的设备资产详情
|
||||
- **WHEN** 页面渲染“绑定卡列表”
|
||||
- **THEN** 每条绑定卡记录 MUST 提供“运营商停机原因”或等效展示位来展示本卡的 `gateway_extend`
|
||||
- **AND** 展示内容 MUST 与该卡返回的 `gateway_extend` 保持一致
|
||||
- **AND** 当某条卡记录的 `gateway_extend` 为空时,该行 MUST 显示稳定占位内容
|
||||
15
openspec/changes/add-asset-gateway-extend-display/tasks.md
Normal file
15
openspec/changes/add-asset-gateway-extend-display/tasks.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## 1. Contract Alignment
|
||||
|
||||
- [x] 1.1 扩展后台资产详情相关类型,支持卡资产详情与设备绑定卡列表中的 `gateway_extend`。
|
||||
- [x] 1.2 调整资产详情数据映射与设备绑定卡刷新合并逻辑,确保 `gateway_extend` 在初次加载和后续刷新后都不会丢失。
|
||||
|
||||
## 2. UI Display
|
||||
|
||||
- [x] 2.1 在卡资产“卡信息”区域新增 `gateway_extend` 展示,作为运营商侧实际停机原因。
|
||||
- [x] 2.2 在设备资产“绑定卡列表”中新增 `gateway_extend` 展示,并为缺失值提供稳定占位内容。
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 验证卡资产详情返回 `gateway_extend` 时页面可按原样展示。
|
||||
- [ ] 3.2 验证设备资产绑定卡列表返回 `gateway_extend` 时每条卡记录展示正确。
|
||||
- [ ] 3.3 验证资产刷新或实时状态更新后,已展示的 `gateway_extend` 不会被错误清空。
|
||||
@@ -57,6 +57,7 @@ export interface AssetResolveResponse {
|
||||
series_name: string // 套餐系列名称
|
||||
real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 已实名
|
||||
realname_policy?: string // 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||
real_name_at?: string | null // 实名时间
|
||||
network_status?: NetworkStatus // 网络状态:0 停机 / 1 开机(仅 card)
|
||||
current_package: string // 当前套餐名称(无则空)
|
||||
current_package_usage_id: number | null // 当前主套餐对应的套餐使用记录 ID,无主套餐时为 null
|
||||
@@ -86,6 +87,7 @@ export interface AssetResolveResponse {
|
||||
supplier?: string // 供应商
|
||||
activation_status?: number // 激活状态
|
||||
enable_polling?: boolean // 是否参与轮询
|
||||
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
||||
|
||||
// ===== 设备专属字段 (asset_type === 'device' 时) =====
|
||||
bound_card_count?: number // 绑定卡数量
|
||||
@@ -117,6 +119,7 @@ export interface AssetBoundCard {
|
||||
realname_policy?: string // 实名认证策略
|
||||
slot_position: number // 插槽位置
|
||||
is_current?: boolean // 是否为设备当前使用的卡
|
||||
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
||||
}
|
||||
|
||||
// ========== 资产实时状态响应 ==========
|
||||
@@ -199,6 +202,7 @@ export type AssetRefreshResponse = AssetRealtimeStatusResponse
|
||||
* 对应接口:GET /api/admin/assets/:asset_type/:id/packages
|
||||
*/
|
||||
export interface AssetPackageUsageRecord {
|
||||
id?: number // 兼容前端当前映射字段
|
||||
package_usage_id?: number // 套餐使用记录 ID
|
||||
package_id?: number // 套餐 ID
|
||||
package_name?: string // 套餐名称
|
||||
@@ -209,8 +213,10 @@ export interface AssetPackageUsageRecord {
|
||||
status_name?: string // 状态中文名
|
||||
real_total_mb?: number // 真流量总量(百分比以此为基准)
|
||||
real_used_mb?: number // 真流量已用
|
||||
real_remaining_mb?: number // 兼容前端当前映射字段
|
||||
virtual_total_mb?: number // 业务停机阈值
|
||||
virtual_used_mb?: number // 展示已用量
|
||||
virtual_remaining_mb?: number // 兼容前端当前映射字段
|
||||
virtual_ratio?: number // 虚流量比例(real/virtual)
|
||||
reduction_pct?: number // 展示增幅比例(小数,如 0.428571)
|
||||
enable_virtual_data?: boolean // 是否启用虚流量
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="运营商停机原因" :span="2">
|
||||
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<template v-if="cardInfo?.bound_device_id">
|
||||
<ElDescriptionsItem label="绑定设备号">
|
||||
<ElButton
|
||||
@@ -160,7 +164,8 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="msisdn" label="MSISDN" min-width="120" />
|
||||
<ElTableColumn prop="msisdn" label="MSISDN" min-width="150" showOverflowTooltip/>
|
||||
|
||||
<ElTableColumn prop="carrier_name" label="运营商" min-width="120" showOverflowTooltip />
|
||||
<ElTableColumn prop="slot_position" label="卡槽位置" width="130">
|
||||
<template #default="scope">
|
||||
@@ -194,7 +199,12 @@
|
||||
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="180">
|
||||
<ElTableColumn label="运营商停机原因" min-width="220" showOverflowTooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.gateway_extend || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<!-- 根据网络状态显示启用或停用按钮 -->
|
||||
<ElButton
|
||||
@@ -437,6 +447,7 @@
|
||||
real_name_status?: number
|
||||
real_name_at?: string
|
||||
realname_policy?: string
|
||||
gateway_extend?: string
|
||||
}
|
||||
|
||||
interface AssetInfo {
|
||||
@@ -461,6 +472,7 @@
|
||||
series_name?: string
|
||||
real_name_at?: string
|
||||
supplier?: string
|
||||
gateway_extend?: string
|
||||
bound_device_id?: number
|
||||
bound_device_no?: string
|
||||
bound_device_name?: string
|
||||
@@ -690,6 +702,10 @@
|
||||
<style scoped lang="scss">
|
||||
.info-card {
|
||||
&.basic-info {
|
||||
.gateway-extend-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import type { AssetWalletResponse, DeviceGatewayInfo, AssetPackageUsageRecord } from '@/types/api'
|
||||
import type {
|
||||
AssetBoundCard,
|
||||
AssetWalletResponse,
|
||||
DeviceGatewayInfo,
|
||||
AssetPackageUsageRecord
|
||||
} from '@/types/api'
|
||||
|
||||
type TrafficDisplaySource = {
|
||||
real_total_mb?: number | null
|
||||
@@ -39,6 +44,23 @@ const getDisplayedUsagePercentage = (traffic?: TrafficDisplaySource | null) => {
|
||||
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 !== null && value !== undefined) {
|
||||
mergedCard[key] = value
|
||||
}
|
||||
}
|
||||
return mergedCard as unknown as AssetBoundCard
|
||||
})
|
||||
|
||||
export function useAssetInfo() {
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
@@ -99,6 +121,7 @@ export function useAssetInfo() {
|
||||
carrier_name: data.carrier_name || '',
|
||||
supplier: data.supplier || '',
|
||||
network_status: data.network_status,
|
||||
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 || '',
|
||||
@@ -293,18 +316,7 @@ export function useAssetInfo() {
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
// 合并实时卡数据与原始卡数据,保留 real_name_at 等静态字段
|
||||
// 仅用实时接口中非 null/undefined 的字段覆盖,避免清掉 resolve 接口返回的静态数据
|
||||
const existingCards: any[] = cardInfo.value.cards || []
|
||||
cardInfo.value.cards = data.cards.map((realtimeCard: any) => {
|
||||
const existing = existingCards.find((c: any) => c.iccid === realtimeCard.iccid)
|
||||
if (!existing) return realtimeCard
|
||||
const merged = { ...existing }
|
||||
for (const [key, value] of Object.entries(realtimeCard)) {
|
||||
if (value !== null && value !== undefined) {
|
||||
merged[key] = value
|
||||
}
|
||||
}
|
||||
return merged
|
||||
})
|
||||
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
||||
}
|
||||
if (data.online_status !== undefined) {
|
||||
cardInfo.value.online_status = data.online_status
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface AssetInfo {
|
||||
sn?: string
|
||||
cards?: BindingCard[]
|
||||
status?: number
|
||||
gateway_extend?: string
|
||||
}
|
||||
|
||||
// 设备绑定卡信息
|
||||
@@ -42,6 +43,7 @@ export interface BindingCard {
|
||||
network_status?: number
|
||||
real_name_status?: number
|
||||
realname_policy?: string
|
||||
gateway_extend?: string
|
||||
}
|
||||
|
||||
// 设备实时信息接口
|
||||
|
||||
Reference in New Issue
Block a user