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 // 套餐系列名称
|
series_name: string // 套餐系列名称
|
||||||
real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 已实名
|
real_name_status: RealNameStatus // 实名状态:0 未实名 / 1 已实名
|
||||||
realname_policy?: string // 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
realname_policy?: string // 实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)
|
||||||
|
real_name_at?: string | null // 实名时间
|
||||||
network_status?: NetworkStatus // 网络状态:0 停机 / 1 开机(仅 card)
|
network_status?: NetworkStatus // 网络状态:0 停机 / 1 开机(仅 card)
|
||||||
current_package: string // 当前套餐名称(无则空)
|
current_package: string // 当前套餐名称(无则空)
|
||||||
current_package_usage_id: number | null // 当前主套餐对应的套餐使用记录 ID,无主套餐时为 null
|
current_package_usage_id: number | null // 当前主套餐对应的套餐使用记录 ID,无主套餐时为 null
|
||||||
@@ -86,6 +87,7 @@ export interface AssetResolveResponse {
|
|||||||
supplier?: string // 供应商
|
supplier?: string // 供应商
|
||||||
activation_status?: number // 激活状态
|
activation_status?: number // 激活状态
|
||||||
enable_polling?: boolean // 是否参与轮询
|
enable_polling?: boolean // 是否参与轮询
|
||||||
|
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
||||||
|
|
||||||
// ===== 设备专属字段 (asset_type === 'device' 时) =====
|
// ===== 设备专属字段 (asset_type === 'device' 时) =====
|
||||||
bound_card_count?: number // 绑定卡数量
|
bound_card_count?: number // 绑定卡数量
|
||||||
@@ -117,6 +119,7 @@ export interface AssetBoundCard {
|
|||||||
realname_policy?: string // 实名认证策略
|
realname_policy?: string // 实名认证策略
|
||||||
slot_position: number // 插槽位置
|
slot_position: number // 插槽位置
|
||||||
is_current?: boolean // 是否为设备当前使用的卡
|
is_current?: boolean // 是否为设备当前使用的卡
|
||||||
|
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 资产实时状态响应 ==========
|
// ========== 资产实时状态响应 ==========
|
||||||
@@ -199,6 +202,7 @@ export type AssetRefreshResponse = AssetRealtimeStatusResponse
|
|||||||
* 对应接口:GET /api/admin/assets/:asset_type/:id/packages
|
* 对应接口:GET /api/admin/assets/:asset_type/:id/packages
|
||||||
*/
|
*/
|
||||||
export interface AssetPackageUsageRecord {
|
export interface AssetPackageUsageRecord {
|
||||||
|
id?: number // 兼容前端当前映射字段
|
||||||
package_usage_id?: number // 套餐使用记录 ID
|
package_usage_id?: number // 套餐使用记录 ID
|
||||||
package_id?: number // 套餐 ID
|
package_id?: number // 套餐 ID
|
||||||
package_name?: string // 套餐名称
|
package_name?: string // 套餐名称
|
||||||
@@ -209,8 +213,10 @@ export interface AssetPackageUsageRecord {
|
|||||||
status_name?: string // 状态中文名
|
status_name?: string // 状态中文名
|
||||||
real_total_mb?: number // 真流量总量(百分比以此为基准)
|
real_total_mb?: number // 真流量总量(百分比以此为基准)
|
||||||
real_used_mb?: number // 真流量已用
|
real_used_mb?: number // 真流量已用
|
||||||
|
real_remaining_mb?: number // 兼容前端当前映射字段
|
||||||
virtual_total_mb?: number // 业务停机阈值
|
virtual_total_mb?: number // 业务停机阈值
|
||||||
virtual_used_mb?: number // 展示已用量
|
virtual_used_mb?: number // 展示已用量
|
||||||
|
virtual_remaining_mb?: number // 兼容前端当前映射字段
|
||||||
virtual_ratio?: number // 虚流量比例(real/virtual)
|
virtual_ratio?: number // 虚流量比例(real/virtual)
|
||||||
reduction_pct?: number // 展示增幅比例(小数,如 0.428571)
|
reduction_pct?: number // 展示增幅比例(小数,如 0.428571)
|
||||||
enable_virtual_data?: boolean // 是否启用虚流量
|
enable_virtual_data?: boolean // 是否启用虚流量
|
||||||
|
|||||||
@@ -67,6 +67,10 @@
|
|||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="运营商停机原因" :span="2">
|
||||||
|
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<template v-if="cardInfo?.bound_device_id">
|
<template v-if="cardInfo?.bound_device_id">
|
||||||
<ElDescriptionsItem label="绑定设备号">
|
<ElDescriptionsItem label="绑定设备号">
|
||||||
<ElButton
|
<ElButton
|
||||||
@@ -160,7 +164,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</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="carrier_name" label="运营商" min-width="120" showOverflowTooltip />
|
||||||
<ElTableColumn prop="slot_position" label="卡槽位置" width="130">
|
<ElTableColumn prop="slot_position" label="卡槽位置" width="130">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -194,7 +199,12 @@
|
|||||||
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
|
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</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">
|
<template #default="scope">
|
||||||
<!-- 根据网络状态显示启用或停用按钮 -->
|
<!-- 根据网络状态显示启用或停用按钮 -->
|
||||||
<ElButton
|
<ElButton
|
||||||
@@ -437,6 +447,7 @@
|
|||||||
real_name_status?: number
|
real_name_status?: number
|
||||||
real_name_at?: string
|
real_name_at?: string
|
||||||
realname_policy?: string
|
realname_policy?: string
|
||||||
|
gateway_extend?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssetInfo {
|
interface AssetInfo {
|
||||||
@@ -461,6 +472,7 @@
|
|||||||
series_name?: string
|
series_name?: string
|
||||||
real_name_at?: string
|
real_name_at?: string
|
||||||
supplier?: string
|
supplier?: string
|
||||||
|
gateway_extend?: string
|
||||||
bound_device_id?: number
|
bound_device_id?: number
|
||||||
bound_device_no?: string
|
bound_device_no?: string
|
||||||
bound_device_name?: string
|
bound_device_name?: string
|
||||||
@@ -690,6 +702,10 @@
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.info-card {
|
.info-card {
|
||||||
&.basic-info {
|
&.basic-info {
|
||||||
|
.gateway-extend-text {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AssetService } from '@/api/modules'
|
import { AssetService } from '@/api/modules'
|
||||||
import type { AssetWalletResponse, DeviceGatewayInfo, AssetPackageUsageRecord } from '@/types/api'
|
import type {
|
||||||
|
AssetBoundCard,
|
||||||
|
AssetWalletResponse,
|
||||||
|
DeviceGatewayInfo,
|
||||||
|
AssetPackageUsageRecord
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
type TrafficDisplaySource = {
|
type TrafficDisplaySource = {
|
||||||
real_total_mb?: number | null
|
real_total_mb?: number | null
|
||||||
@@ -39,6 +44,23 @@ const getDisplayedUsagePercentage = (traffic?: TrafficDisplaySource | null) => {
|
|||||||
return totalMb > 0 ? `${((usedMb / totalMb) * 100).toFixed(2)}%` : '0.00%'
|
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() {
|
export function useAssetInfo() {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -99,6 +121,7 @@ export function useAssetInfo() {
|
|||||||
carrier_name: data.carrier_name || '',
|
carrier_name: data.carrier_name || '',
|
||||||
supplier: data.supplier || '',
|
supplier: data.supplier || '',
|
||||||
network_status: data.network_status,
|
network_status: data.network_status,
|
||||||
|
gateway_extend: data.gateway_extend ?? '',
|
||||||
bound_device_id: data.bound_device_id,
|
bound_device_id: data.bound_device_id,
|
||||||
bound_device_no: data.bound_device_no || '',
|
bound_device_no: data.bound_device_no || '',
|
||||||
bound_device_name: data.bound_device_name || '',
|
bound_device_name: data.bound_device_name || '',
|
||||||
@@ -293,18 +316,7 @@ export function useAssetInfo() {
|
|||||||
if (data.cards && data.cards.length > 0) {
|
if (data.cards && data.cards.length > 0) {
|
||||||
// 合并实时卡数据与原始卡数据,保留 real_name_at 等静态字段
|
// 合并实时卡数据与原始卡数据,保留 real_name_at 等静态字段
|
||||||
// 仅用实时接口中非 null/undefined 的字段覆盖,避免清掉 resolve 接口返回的静态数据
|
// 仅用实时接口中非 null/undefined 的字段覆盖,避免清掉 resolve 接口返回的静态数据
|
||||||
const existingCards: any[] = cardInfo.value.cards || []
|
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (data.online_status !== undefined) {
|
if (data.online_status !== undefined) {
|
||||||
cardInfo.value.online_status = data.online_status
|
cardInfo.value.online_status = data.online_status
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface AssetInfo {
|
|||||||
sn?: string
|
sn?: string
|
||||||
cards?: BindingCard[]
|
cards?: BindingCard[]
|
||||||
status?: number
|
status?: number
|
||||||
|
gateway_extend?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备绑定卡信息
|
// 设备绑定卡信息
|
||||||
@@ -42,6 +43,7 @@ export interface BindingCard {
|
|||||||
network_status?: number
|
network_status?: number
|
||||||
real_name_status?: number
|
real_name_status?: number
|
||||||
realname_policy?: string
|
realname_policy?: string
|
||||||
|
gateway_extend?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备实时信息接口
|
// 设备实时信息接口
|
||||||
|
|||||||
Reference in New Issue
Block a user