feat: 新增卡视角以及设备视角的实名同步时间,卡状态同步时间,流量同步时间
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m56s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m56s
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Change: 更新资产运营商状态文案与区间分配数量提示
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
当前后台“资产信息”页的“卡信息”区域会把 `gateway_extend` 展示为“运营商停机原因”,但最新业务要求需要统一改为更宽泛的“运营商状态”文案。
|
||||||
|
|
||||||
|
同时,IoT 卡管理和设备管理里的“批量设置套餐系列绑定”弹窗已经支持区间选择,但在输入起始/结束标识后没有给出本次将覆盖多少张卡或多少台设备的直观提示,运营人员需要手工心算范围,容易误判分配数量。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 调整资产信息卡资产详情中“卡信息”区域的 `gateway_extend` 展示标签,将“运营商停机原因”改为 `运营商状态`
|
||||||
|
- 为 IoT 卡“批量设置套餐系列绑定”弹窗的区间模式新增提示文案:`共分配xxx张卡`
|
||||||
|
- 计算方式为 `结束ICCID - 起始ICCID + 1`
|
||||||
|
- 仅在起止值可计算且结果为正整数时显示
|
||||||
|
- 为设备管理“批量设置设备套餐系列绑定”弹窗的区间模式新增提示文案:`共分配xxx台设备`
|
||||||
|
- 计算方式为 `结束设备号 - 起始设备号 + 1`
|
||||||
|
- 仅在起止值可计算且结果为正整数时显示
|
||||||
|
- 不调整后端接口字段,仅补充前端展示与交互约束
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `asset-information`
|
||||||
|
- `iot-card-series-binding`
|
||||||
|
- `device-series-binding`
|
||||||
|
- Affected code:
|
||||||
|
- `src/views/asset-management/asset-information/components/BasicInfoCard.vue`
|
||||||
|
- `src/views/asset-management/iot-card-management/index.vue`
|
||||||
|
- `src/views/asset-management/device-list/index.vue`
|
||||||
|
- Dependencies:
|
||||||
|
- `add-asset-gateway-extend-display`
|
||||||
|
- `update-batch-series-binding-contract`
|
||||||
|
- Breaking changes:
|
||||||
|
- 无外部接口破坏性变更;本次仅调整文案和区间模式提示
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Admin Asset Card Carrier Status Label
|
||||||
|
|
||||||
|
The admin asset information view SHALL label `gateway_extend` as `运营商状态` within the card information section.
|
||||||
|
|
||||||
|
#### Scenario: Show carrier status label in card information
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||||
|
- **WHEN** 页面渲染“卡信息”区域中的 `gateway_extend`
|
||||||
|
- **THEN** 字段标签 MUST 显示为 `运营商状态`
|
||||||
|
- **AND** 字段值 MUST 继续直接展示上游返回的 `gateway_extend`
|
||||||
|
- **AND** 当 `gateway_extend` 为空时,页面 MUST 显示稳定的占位内容
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Device Batch Series Binding Range Allocation Hint
|
||||||
|
|
||||||
|
The device batch series binding dialog SHALL provide a derived allocation-count hint for device-number range selection.
|
||||||
|
|
||||||
|
The hint MUST appear immediately below the `virtual_no_start` and `virtual_no_end` inputs.
|
||||||
|
|
||||||
|
The count MUST be computed as `结束设备号 - 起始设备号 + 1` using the same numeric range values accepted by the dialog.
|
||||||
|
|
||||||
|
#### Scenario: Show allocation count for a valid device-number range
|
||||||
|
|
||||||
|
- **GIVEN** 用户打开“批量设置设备套餐系列绑定”弹窗
|
||||||
|
- **AND** 用户选择 `selection_type="range"`
|
||||||
|
- **WHEN** 用户输入 `virtual_no_start` 和 `virtual_no_end`,且计算结果为正整数
|
||||||
|
- **THEN** 弹窗 MUST 显示 `共分配xxx台设备`
|
||||||
|
- **AND** `xxx` MUST 等于 `结束设备号 - 起始设备号 + 1`
|
||||||
|
- **AND** 当任一端点变化时,提示 MUST 立即更新
|
||||||
|
|
||||||
|
#### Scenario: Hide count hint for incomplete or invalid device-number ranges
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择 `selection_type="range"`
|
||||||
|
- **WHEN** 任一起止设备号为空、无法按有效数值范围计算,或计算结果小于 `1`
|
||||||
|
- **THEN** 弹窗 MUST NOT 显示误导性的分配数量提示
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: IoT Card Batch Series Binding Range Allocation Hint
|
||||||
|
|
||||||
|
The IoT card batch series binding dialog SHALL provide a derived allocation-count hint for ICCID range selection.
|
||||||
|
|
||||||
|
The hint MUST appear immediately below the `iccid_start` and `iccid_end` inputs.
|
||||||
|
|
||||||
|
The count MUST be computed with exact integer arithmetic as `iccid_end - iccid_start + 1` so that long ICCID values do not lose precision.
|
||||||
|
|
||||||
|
#### Scenario: Show allocation count for a valid ICCID range
|
||||||
|
|
||||||
|
- **GIVEN** 用户打开“批量设置套餐系列绑定”弹窗
|
||||||
|
- **AND** 用户选择 `selection_type="range"`
|
||||||
|
- **WHEN** 用户输入 `iccid_start` 和 `iccid_end`,且计算结果为正整数
|
||||||
|
- **THEN** 弹窗 MUST 显示 `共分配xxx张卡`
|
||||||
|
- **AND** `xxx` MUST 等于 `iccid_end - iccid_start + 1`
|
||||||
|
- **AND** 当任一端点变化时,提示 MUST 立即更新
|
||||||
|
|
||||||
|
#### Scenario: Hide count hint for incomplete or invalid ICCID ranges
|
||||||
|
|
||||||
|
- **GIVEN** 用户选择 `selection_type="range"`
|
||||||
|
- **WHEN** 任一起止 ICCID 为空、无法按整数计算,或计算结果小于 `1`
|
||||||
|
- **THEN** 弹窗 MUST NOT 显示误导性的分配数量提示
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## 1. Asset Information
|
||||||
|
|
||||||
|
- [x] 1.1 将资产信息“卡信息”区域的 `gateway_extend` 标签从“运营商停机原因”改为 `运营商状态`
|
||||||
|
- [x] 1.2 确认该字段仍直接展示 `gateway_extend` 原值,并保留空值占位行为
|
||||||
|
|
||||||
|
## 2. Range Allocation Hints
|
||||||
|
|
||||||
|
- [x] 2.1 在 IoT 卡“批量设置套餐系列绑定”弹窗的区间模式下,于起始/结束 ICCID 输入框下方新增 `共分配xxx张卡` 提示
|
||||||
|
- [x] 2.2 在设备管理“批量设置设备套餐系列绑定”弹窗的区间模式下,于起始/结束设备号输入框下方新增 `共分配xxx台设备` 提示
|
||||||
|
- [x] 2.3 让两个提示都随起止值变化实时更新,并在范围不完整、不可计算或结果小于 1 时隐藏
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 验证卡资产详情“卡信息”区域展示标签为 `运营商状态`
|
||||||
|
- [x] 3.2 验证 ICCID 区间提示按 `结束ICCID - 起始ICCID + 1` 计算且不出现精度丢失
|
||||||
|
- [x] 3.3 验证设备号区间提示按 `结束设备号 - 起始设备号 + 1` 计算,并在无效范围时不显示误导性数量
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Change: 更新后台资产信息同步时间展示
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
当前后台“资产信息”页虽然已经展示卡状态、实名状态和部分实时信息,但还缺少运营判断数据新鲜度所需的三个时间字段:实名同步时间、卡状态同步时间、流量同步时间。
|
||||||
|
|
||||||
|
- 卡资产的 `GET /api/admin/assets/:identifier/realtime-status` 已在顶层返回 `last_real_name_check_at`、`last_card_status_check_at`、`last_data_check_at`,但前端未展示。
|
||||||
|
- 设备资产的同一接口会在 `cards` 列表中按绑定卡返回这三个字段;当前“设备实时信息”区块只消费 `device_realtime`,无法展示当前使用卡的同步时间。
|
||||||
|
|
||||||
|
如果不补齐这三个字段,后台在卡页和设备页都无法快速确认上游同步是否及时,也不利于定位实名、停复机和流量状态的延迟问题。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 更新后台“资产信息”的卡资产“卡信息”区域:
|
||||||
|
- 新增 `实名同步时间`,映射 `last_real_name_check_at`
|
||||||
|
- 新增 `卡状态同步时间`,映射 `last_card_status_check_at`
|
||||||
|
- 新增 `流量同步时间`,映射 `last_data_check_at`
|
||||||
|
- 更新后台“资产信息”的设备资产“设备实时信息”区域:
|
||||||
|
- 直接读取设备实时接口 `data` 顶层的 `last_real_name_check_at`、`last_card_status_check_at`、`last_data_check_at`
|
||||||
|
- 当对应字段为空时,显示稳定占位内容
|
||||||
|
- 更新前端资产实时状态契约和数据合并逻辑:
|
||||||
|
- 支持卡资产顶层这三个同步时间字段
|
||||||
|
- 支持设备绑定卡记录中的这三个同步时间字段
|
||||||
|
- 刷新后合并绑定卡实时数据时,不得丢失这三个字段
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `asset-information`
|
||||||
|
- Affected code:
|
||||||
|
- `src/types/api/asset.ts`
|
||||||
|
- `src/views/asset-management/asset-information/types.ts`
|
||||||
|
- `src/views/asset-management/asset-information/composables/useAssetInfo.ts`
|
||||||
|
- `src/views/asset-management/asset-information/components/BasicInfoCard.vue`
|
||||||
|
- Dependencies:
|
||||||
|
- 与活跃变更 `update-admin-asset-device-signal-and-audit-logs` 共享 `asset-information` 能力和 `BasicInfoCard.vue` / `useAssetInfo.ts` 文件范围
|
||||||
|
- 与活跃变更 `update-order-upload-and-device-switch-mode` 共享设备实时信息区块
|
||||||
|
- 依赖后端继续按当前接口返回这三个时间字段,无需新增接口
|
||||||
|
- Breaking changes:
|
||||||
|
- 无外部接口 breaking change;本次仅为前端展示契约和页面展示的增量调整
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Admin Asset Card Sync Timestamp Display
|
||||||
|
|
||||||
|
The admin asset information view SHALL display realtime sync timestamps for card assets within the card information section.
|
||||||
|
|
||||||
|
#### Scenario: Show card sync timestamps on card asset page
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||||
|
- **AND** `GET /api/admin/assets/:identifier/realtime-status` 或 `POST /api/admin/assets/:identifier/refresh` 返回了卡资产实时数据
|
||||||
|
- **WHEN** 页面渲染“卡信息”区域或完成实时刷新后的状态更新
|
||||||
|
- **THEN** 页面 MUST 显示 `实名同步时间`,并映射 `last_real_name_check_at`
|
||||||
|
- **AND** 页面 MUST 显示 `卡状态同步时间`,并映射 `last_card_status_check_at`
|
||||||
|
- **AND** 页面 MUST 显示 `流量同步时间`,并映射 `last_data_check_at`
|
||||||
|
- **AND** 这三个字段的时间格式 MUST 与页面其他时间字段保持一致
|
||||||
|
|
||||||
|
#### Scenario: Card sync timestamps show placeholders when unavailable
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||||
|
- **WHEN** 任一同步时间字段为 `null`、空字符串或接口未返回对应值
|
||||||
|
- **THEN** 页面 MUST 保留对应字段标签
|
||||||
|
- **AND** 页面 MUST 为该字段显示稳定的占位内容
|
||||||
|
|
||||||
|
### Requirement: Admin Device Realtime Sync Timestamp Display
|
||||||
|
|
||||||
|
The admin asset information view SHALL display device realtime sync timestamps from top-level realtime response fields within the device realtime information section for device assets.
|
||||||
|
|
||||||
|
#### Scenario: Show device realtime sync timestamps in device realtime information
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看后台“资产信息”中的设备资产详情
|
||||||
|
- **AND** `GET /api/admin/assets/:identifier/realtime-status` 或 `POST /api/admin/assets/:identifier/refresh` 返回了设备资产实时数据
|
||||||
|
- **WHEN** 页面渲染“设备实时信息”区域或完成实时刷新后的状态更新
|
||||||
|
- **THEN** 页面 MUST 显示 `实名同步时间`,并映射顶层 `last_real_name_check_at`
|
||||||
|
- **AND** 页面 MUST 显示 `卡状态同步时间`,并映射顶层 `last_card_status_check_at`
|
||||||
|
- **AND** 页面 MUST 显示 `流量同步时间`,并映射顶层 `last_data_check_at`
|
||||||
|
- **AND** 页面 MUST NOT 要求这三个字段必须存在于 `device_realtime` 中才可展示
|
||||||
|
|
||||||
|
#### Scenario: Device realtime section keeps stable placeholders when top-level timestamps are empty
|
||||||
|
|
||||||
|
- **GIVEN** 用户正在查看后台“资产信息”中的设备资产详情
|
||||||
|
- **WHEN** 顶层 `last_real_name_check_at`、`last_card_status_check_at`、`last_data_check_at` 中任一字段为空
|
||||||
|
- **THEN** 页面 MUST 保留 `实名同步时间`、`卡状态同步时间`、`流量同步时间` 这三个字段标签
|
||||||
|
- **AND** 页面 MUST 为缺失值显示稳定的占位内容
|
||||||
|
|
||||||
|
### Requirement: Admin Asset Realtime Sync Timestamp Contract Compatibility
|
||||||
|
|
||||||
|
The admin asset information integration SHALL accept and preserve additive sync timestamp fields from realtime responses for card assets and device bound cards.
|
||||||
|
|
||||||
|
#### Scenario: Card realtime contract accepts additive sync timestamp fields
|
||||||
|
|
||||||
|
- **GIVEN** `GET /api/admin/assets/:identifier/realtime-status` 或 `POST /api/admin/assets/:identifier/refresh` 返回卡资产实时数据
|
||||||
|
- **WHEN** 前端解析实时状态响应
|
||||||
|
- **THEN** 前端类型契约 MUST 支持顶层 `last_data_check_at`
|
||||||
|
- **AND** 前端类型契约 MUST 支持顶层 `last_real_name_check_at`
|
||||||
|
- **AND** 前端类型契约 MUST 支持顶层 `last_card_status_check_at`
|
||||||
|
|
||||||
|
#### Scenario: Device bound-card merge preserves sync timestamp fields
|
||||||
|
|
||||||
|
- **GIVEN** `GET /api/admin/assets/:identifier/realtime-status` 或 `POST /api/admin/assets/:identifier/refresh` 返回设备资产实时数据
|
||||||
|
- **AND** `cards` 列表中的绑定卡记录包含 `last_data_check_at`、`last_real_name_check_at`、`last_card_status_check_at`
|
||||||
|
- **WHEN** 前端将实时绑定卡数据与已有设备绑定卡状态合并
|
||||||
|
- **THEN** 每条绑定卡记录 MUST 保留各自的三个同步时间字段
|
||||||
|
- **AND** 绑定卡同步时间字段 MAY 被其他设备卡展示或后续状态使用
|
||||||
|
- **AND** 合并过程 MUST NOT 因刷新而无故丢失已返回的同步时间值
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Tasks: Update Asset Realtime Sync Timestamps Display
|
||||||
|
|
||||||
|
## 1. Contract And State
|
||||||
|
|
||||||
|
- [x] 1.1 扩展资产实时状态相关类型,支持卡资产顶层和设备绑定卡记录中的 `last_data_check_at`、`last_real_name_check_at`、`last_card_status_check_at`。
|
||||||
|
- [x] 1.2 调整资产实时状态合并逻辑,确保设备绑定卡刷新后保留上述三个同步时间字段。
|
||||||
|
- [x] 1.3 为设备页补充设备实时状态顶层同步时间的读取逻辑,避免“设备实时信息”仅依赖 `device_realtime`。
|
||||||
|
|
||||||
|
## 2. UI Display
|
||||||
|
|
||||||
|
- [x] 2.1 在卡资产“卡信息”区域新增 `实名同步时间`、`卡状态同步时间`、`流量同步时间` 三个展示项。
|
||||||
|
- [x] 2.2 在设备资产“设备实时信息”区域新增同名三个展示项,并展示设备实时状态顶层返回的对应时间。
|
||||||
|
- [x] 2.3 当字段为空或实时接口未返回对应值时,统一显示稳定占位内容。
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [ ] 3.1 验证卡资产 `GET /api/admin/assets/:identifier/realtime-status` 返回的三个同步时间可正确展示在“卡信息”区域。
|
||||||
|
- [ ] 3.2 验证设备资产 `GET /api/admin/assets/:identifier/realtime-status` 返回的顶层三个同步时间可正确展示在“设备实时信息”区域。
|
||||||
|
- [ ] 3.3 验证 `POST /api/admin/assets/:identifier/refresh` 后卡页和设备页的三个同步时间展示保持一致。
|
||||||
|
- [x] 3.4 运行 `openspec validate update-asset-realtime-sync-timestamps-display --strict`。
|
||||||
@@ -60,7 +60,7 @@ export interface AgentRechargeListResponse {
|
|||||||
|
|
||||||
// 创建代理充值订单请求
|
// 创建代理充值订单请求
|
||||||
export interface CreateAgentRechargeRequest {
|
export interface CreateAgentRechargeRequest {
|
||||||
amount: number // 充值金额(单位:分),范围 10000 ~ 100000000
|
amount: number // 充值金额(单位:分),最小 1(即 ¥0.01)
|
||||||
payment_method: AgentRechargePaymentMethod
|
payment_method: AgentRechargePaymentMethod
|
||||||
shop_id: number
|
shop_id: number
|
||||||
payment_voucher_key?: string // 线下支付凭证(payment_method=offline 时必填)
|
payment_voucher_key?: string // 线下支付凭证(payment_method=offline 时必填)
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ export interface AssetBoundCard {
|
|||||||
slot_position: number // 插槽位置
|
slot_position: number // 插槽位置
|
||||||
is_current?: boolean // 是否为设备当前使用的卡
|
is_current?: boolean // 是否为设备当前使用的卡
|
||||||
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
gateway_extend?: string // Gateway 卡状态扩展字段,原样返回上游 extend
|
||||||
|
last_data_check_at?: string | null // 流量同步时间
|
||||||
|
last_real_name_check_at?: string | null // 实名同步时间
|
||||||
|
last_card_status_check_at?: string | null // 卡状态同步时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 资产实时状态响应 ==========
|
// ========== 资产实时状态响应 ==========
|
||||||
@@ -179,7 +182,10 @@ export interface AssetRealtimeStatusResponse {
|
|||||||
realname_policy?: string // 实名认证策略(仅 card)
|
realname_policy?: string // 实名认证策略(仅 card)
|
||||||
current_month_usage_mb?: number // 本月已用流量 MB(仅 card)
|
current_month_usage_mb?: number // 本月已用流量 MB(仅 card)
|
||||||
last_gateway_reading_mb?: number // 运营商周期月内已用流量 MB(仅 card)
|
last_gateway_reading_mb?: number // 运营商周期月内已用流量 MB(仅 card)
|
||||||
last_sync_time?: string // 最后同步时间(仅 card)
|
last_sync_time?: string | null // 最后同步时间(仅 card)
|
||||||
|
last_data_check_at?: string | null // 流量同步时间
|
||||||
|
last_real_name_check_at?: string | null // 实名同步时间
|
||||||
|
last_card_status_check_at?: string | null // 卡状态同步时间
|
||||||
|
|
||||||
// ===== 设备专属字段 =====
|
// ===== 设备专属字段 =====
|
||||||
device_protect_status?: DeviceProtectStatus // 保护期(仅 device)
|
device_protect_status?: DeviceProtectStatus // 保护期(仅 device)
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
<ElDescriptionsItem label="实名时间">{{
|
<ElDescriptionsItem label="实名时间">{{
|
||||||
cardInfo?.real_name_at ? formatDateTime(cardInfo.real_name_at) : '-'
|
cardInfo?.real_name_at ? formatDateTime(cardInfo.real_name_at) : '-'
|
||||||
}}</ElDescriptionsItem>
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="实名同步时间">{{
|
||||||
|
formatDateTime(cardInfo?.last_real_name_check_at) || '-'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="MSISDN">{{ cardInfo?.msisdn || '-' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="MSISDN">{{ cardInfo?.msisdn || '-' }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="资产状态">
|
<ElDescriptionsItem label="资产状态">
|
||||||
@@ -66,8 +69,14 @@
|
|||||||
{{ getAssetStatusName(cardInfo?.status) }}
|
{{ getAssetStatusName(cardInfo?.status) }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="卡状态同步时间">{{
|
||||||
|
formatDateTime(cardInfo?.last_card_status_check_at) || '-'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="流量同步时间">{{
|
||||||
|
formatDateTime(cardInfo?.last_data_check_at) || '-'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="运营商停机原因" :span="2">
|
<ElDescriptionsItem label="运营商状态" :span="1">
|
||||||
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
|
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
@@ -246,96 +255,117 @@
|
|||||||
|
|
||||||
<!-- 设备实时信息 -->
|
<!-- 设备实时信息 -->
|
||||||
<ElDivider content-position="left">设备实时信息</ElDivider>
|
<ElDivider content-position="left">设备实时信息</ElDivider>
|
||||||
<div v-if="deviceRealtime" v-loading="realtimeLoading" style="margin-top: 16px">
|
<div v-if="hasDeviceRealtimeSection" v-loading="realtimeLoading" style="margin-top: 16px">
|
||||||
<ElDescriptions :column="4" border>
|
<ElDescriptions :column="4" border>
|
||||||
<!-- 基本状态 -->
|
<!-- 基本状态 -->
|
||||||
<ElDescriptionsItem label="在线状态">
|
<ElDescriptionsItem label="在线状态">
|
||||||
<ElTag :type="deviceRealtime.online_status === 1 ? 'success' : 'danger'" size="small">
|
<ElTag :type="getOnlineStatusType(deviceRealtime?.online_status)" size="small">
|
||||||
{{ deviceRealtime.online_status === 1 ? '在线' : '离线' }}
|
{{ getOnlineStatusName(deviceRealtime?.online_status) }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="电量">
|
<ElDescriptionsItem label="电量">
|
||||||
{{
|
{{
|
||||||
deviceRealtime.battery_level !== null && deviceRealtime.battery_level !== undefined
|
deviceRealtime?.battery_level !== null && deviceRealtime?.battery_level !== undefined
|
||||||
? `${deviceRealtime.battery_level}%`
|
? `${deviceRealtime?.battery_level}%`
|
||||||
: '-'
|
: '-'
|
||||||
}}
|
}}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<!-- 时间信息 -->
|
<!-- 时间信息 -->
|
||||||
<ElDescriptionsItem label="本次开机时长">
|
<ElDescriptionsItem label="本次开机时长">
|
||||||
{{ formatDuration(deviceRealtime.run_time) }}
|
{{ formatDuration(deviceRealtime?.run_time) }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="本次联网时长">
|
<ElDescriptionsItem label="本次联网时长">
|
||||||
{{ formatDuration(deviceRealtime.connect_time) }}
|
{{ formatDuration(deviceRealtime?.connect_time) }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="最后在线时间">
|
<ElDescriptionsItem label="最后在线时间">
|
||||||
{{ formatDateTime(deviceRealtime.last_online_time) || '-' }}
|
{{ formatDateTime(deviceRealtime?.last_online_time) || '-' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="实名同步时间">
|
||||||
|
{{ formatDateTime(cardInfo?.last_real_name_check_at) || '-' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="卡状态同步时间">
|
||||||
|
{{ formatDateTime(cardInfo?.last_card_status_check_at) || '-' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="流量同步时间">
|
||||||
|
{{ formatDateTime(cardInfo?.last_data_check_at) || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<!-- 信号信息 -->
|
<!-- 信号信息 -->
|
||||||
<ElDescriptionsItem label="信号综合判断">
|
<ElDescriptionsItem label="信号综合判断">
|
||||||
<ElTag :type="getSignalQualityType(deviceRealtime.signal_quality)" size="small">
|
<ElTag :type="getSignalQualityType(deviceRealtime?.signal_quality)" size="small">
|
||||||
{{ getSignalQualityText(deviceRealtime.signal_quality) }}
|
{{ getSignalQualityText(deviceRealtime?.signal_quality) }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="信号较弱原因" :span="3">
|
<ElDescriptionsItem label="信号较弱原因" :span="3">
|
||||||
{{ getSignalBadReasonText(deviceRealtime.signal_bad_reason) }}
|
{{ getSignalBadReasonText(deviceRealtime?.signal_bad_reason) }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="已连接设备数">
|
<ElDescriptionsItem label="已连接设备数">
|
||||||
{{ deviceRealtime.client_number ?? '-' }} / {{ deviceRealtime.max_clients ?? '-' }}
|
{{ deviceRealtime?.client_number ?? '-' }} / {{ deviceRealtime?.max_clients ?? '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<!-- WiFi 信息 -->
|
<!-- WiFi 信息 -->
|
||||||
<ElDescriptionsItem label="WiFi 状态">
|
<ElDescriptionsItem label="WiFi 状态">
|
||||||
<ElTag :type="deviceRealtime.wifi_enabled ? 'success' : 'info'" size="small">
|
<ElTag :type="deviceRealtime?.wifi_enabled ? 'success' : 'info'" size="small">
|
||||||
{{ deviceRealtime.wifi_enabled ? '已开启' : '已关闭' }}
|
{{
|
||||||
|
deviceRealtime?.wifi_enabled === undefined
|
||||||
|
? '-'
|
||||||
|
: deviceRealtime.wifi_enabled
|
||||||
|
? '已开启'
|
||||||
|
: '已关闭'
|
||||||
|
}}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="WiFi 名称">
|
<ElDescriptionsItem label="WiFi 名称">
|
||||||
{{ deviceRealtime.ssid || '-' }}
|
{{ deviceRealtime?.ssid || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="WiFi 密码">
|
<ElDescriptionsItem label="WiFi 密码">
|
||||||
{{ deviceRealtime.wifi_password || '-' }}
|
{{ deviceRealtime?.wifi_password || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<!-- 网络信息 -->
|
<!-- 网络信息 -->
|
||||||
<ElDescriptionsItem label="WAN IP">
|
<ElDescriptionsItem label="WAN IP">
|
||||||
{{ deviceRealtime.wan_ip || '-' }}
|
{{ deviceRealtime?.wan_ip || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="LAN IP">
|
<ElDescriptionsItem label="LAN IP">
|
||||||
{{ deviceRealtime.lan_ip || '-' }}
|
{{ deviceRealtime?.lan_ip || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="MAC 地址">
|
<ElDescriptionsItem label="MAC 地址">
|
||||||
{{ deviceRealtime.mac_address || '-' }}
|
{{ deviceRealtime?.mac_address || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="IMSI">
|
<ElDescriptionsItem label="IMSI">
|
||||||
{{ deviceRealtime.imsi || '-' }}
|
{{ deviceRealtime?.imsi || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<!-- 设备配置 -->
|
<!-- 设备配置 -->
|
||||||
<ElDescriptionsItem label="限速">
|
<ElDescriptionsItem label="限速">
|
||||||
{{ deviceRealtime.limit_speed ? `${deviceRealtime.limit_speed} KB/s` : '不限速' }}
|
{{
|
||||||
|
deviceRealtime?.limit_speed === undefined
|
||||||
|
? '-'
|
||||||
|
: deviceRealtime.limit_speed
|
||||||
|
? `${deviceRealtime.limit_speed} KB/s`
|
||||||
|
: '不限速'
|
||||||
|
}}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="固件版本">
|
<ElDescriptionsItem label="固件版本">
|
||||||
{{ deviceRealtime.software_version || '-' }}
|
{{ deviceRealtime?.software_version || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="切卡模式">
|
<ElDescriptionsItem label="切卡模式">
|
||||||
{{
|
{{
|
||||||
deviceRealtime.switch_mode === '0'
|
deviceRealtime?.switch_mode === '0'
|
||||||
? '自动'
|
? '自动'
|
||||||
: deviceRealtime.switch_mode === '1'
|
: deviceRealtime?.switch_mode === '1'
|
||||||
? '手动'
|
? '手动'
|
||||||
: '-'
|
: '-'
|
||||||
}}
|
}}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="上报周期">
|
<ElDescriptionsItem label="上报周期">
|
||||||
{{ deviceRealtime.sync_interval ? `${deviceRealtime.sync_interval} 秒` : '-' }}
|
{{ deviceRealtime?.sync_interval ? `${deviceRealtime.sync_interval} 秒` : '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="信息更新时间">
|
<ElDescriptionsItem label="信息更新时间">
|
||||||
{{ formatDateTime(deviceRealtime.last_update_time) || '-' }}
|
{{ formatDateTime(deviceRealtime?.last_update_time) || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
</div>
|
</div>
|
||||||
@@ -451,6 +481,9 @@
|
|||||||
real_name_at?: string
|
real_name_at?: string
|
||||||
realname_policy?: string
|
realname_policy?: string
|
||||||
gateway_extend?: string
|
gateway_extend?: string
|
||||||
|
last_data_check_at?: string | null
|
||||||
|
last_real_name_check_at?: string | null
|
||||||
|
last_card_status_check_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssetInfo {
|
interface AssetInfo {
|
||||||
@@ -477,6 +510,10 @@
|
|||||||
real_name_at?: string
|
real_name_at?: string
|
||||||
supplier?: string
|
supplier?: string
|
||||||
gateway_extend?: string
|
gateway_extend?: string
|
||||||
|
last_sync_time?: string | null
|
||||||
|
last_data_check_at?: string | null
|
||||||
|
last_real_name_check_at?: string | null
|
||||||
|
last_card_status_check_at?: string | null
|
||||||
bound_device_id?: number
|
bound_device_id?: number
|
||||||
bound_device_no?: string
|
bound_device_no?: string
|
||||||
bound_device_name?: string
|
bound_device_name?: string
|
||||||
@@ -496,13 +533,13 @@
|
|||||||
battery_level?: number | null
|
battery_level?: number | null
|
||||||
run_time?: string
|
run_time?: string
|
||||||
connect_time?: string
|
connect_time?: string
|
||||||
last_online_time?: string
|
last_online_time?: string | null
|
||||||
client_number?: number
|
client_number?: number
|
||||||
max_clients?: number
|
max_clients?: number
|
||||||
current_iccid?: string
|
current_iccid?: string
|
||||||
signal_quality?: string
|
signal_quality?: string
|
||||||
signal_bad_reason?: string
|
signal_bad_reason?: string
|
||||||
wifi_enabled?: boolean
|
wifi_enabled?: boolean | number
|
||||||
ssid?: string
|
ssid?: string
|
||||||
wifi_password?: string
|
wifi_password?: string
|
||||||
ip_address?: string
|
ip_address?: string
|
||||||
@@ -516,8 +553,8 @@
|
|||||||
switch_mode?: string
|
switch_mode?: string
|
||||||
sync_interval?: number
|
sync_interval?: number
|
||||||
device_id?: string
|
device_id?: string
|
||||||
last_sync_time?: string
|
last_sync_time?: string | null
|
||||||
last_update_time?: string
|
last_update_time?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -610,6 +647,14 @@
|
|||||||
return props.cardInfo.cards.find((card: BindingCard) => card.is_current) || null
|
return props.cardInfo.cards.find((card: BindingCard) => card.is_current) || null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasDeviceRealtimeSection = computed(
|
||||||
|
() =>
|
||||||
|
Boolean(props.deviceRealtime) ||
|
||||||
|
props.cardInfo?.last_data_check_at !== undefined ||
|
||||||
|
props.cardInfo?.last_real_name_check_at !== undefined ||
|
||||||
|
props.cardInfo?.last_card_status_check_at !== undefined
|
||||||
|
)
|
||||||
|
|
||||||
// 处理轮询开关变化
|
// 处理轮询开关变化
|
||||||
const handlePollingChange = (value: string | number | boolean) => {
|
const handlePollingChange = (value: string | number | boolean) => {
|
||||||
emit('update:pollingEnabled', Boolean(value))
|
emit('update:pollingEnabled', Boolean(value))
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{{ data.package_name }}
|
{{ data.package_name }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="总使用流量" :span="2">
|
<ElDescriptionsItem label="总使用流量" :span="2">
|
||||||
{{ formatDataSize(data.total_usage_mb) }}
|
{{ formatDataSize(displayedTotalUsageMb) }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
@@ -59,9 +59,9 @@
|
|||||||
<ElTableColumn label="使用进度" min-width="200">
|
<ElTableColumn label="使用进度" min-width="200">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElProgress
|
<ElProgress
|
||||||
:percentage="getUsageProgress(row.cumulative_usage_mb, data.total_usage_mb)"
|
:percentage="getUsageProgress(row.cumulative_usage_mb, displayedTotalUsageMb)"
|
||||||
:color="
|
:color="
|
||||||
getProgressColor(getUsageProgress(row.cumulative_usage_mb, data.total_usage_mb))
|
getProgressColor(getUsageProgress(row.cumulative_usage_mb, displayedTotalUsageMb))
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,14 +86,28 @@
|
|||||||
ElTable,
|
ElTable,
|
||||||
ElTableColumn,
|
ElTableColumn,
|
||||||
ElProgress,
|
ElProgress,
|
||||||
ElEmpty,
|
ElEmpty
|
||||||
ElMessage
|
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { CardService } from '@/api/modules'
|
import { CardService } from '@/api/modules'
|
||||||
|
import type { PackageInfo } from '../../types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
packageUsageId?: number
|
packageUsageId?: number
|
||||||
|
packageRow?: PackageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DailyRecordItem {
|
||||||
|
date: string
|
||||||
|
daily_usage_mb: number
|
||||||
|
cumulative_usage_mb: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DailyRecordsResponseData {
|
||||||
|
package_name: string
|
||||||
|
package_usage_id: number
|
||||||
|
total_usage_mb: number
|
||||||
|
records: DailyRecordItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -104,16 +118,19 @@
|
|||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const data = ref<any>(null)
|
const data = ref<DailyRecordsResponseData | null>(null)
|
||||||
const filterType = ref<'all' | 'day' | 'month'>('all')
|
const filterType = ref<'all' | 'day' | 'month'>('all')
|
||||||
const selectedDate = ref<string>('')
|
const selectedDate = ref<string>('')
|
||||||
const selectedMonth = ref<string>('')
|
const selectedMonth = ref<string>('')
|
||||||
const filteredRecords = ref<any[]>([])
|
const filteredRecords = ref<DailyRecordItem[]>([])
|
||||||
|
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
const displayedTotalUsageMb = computed(
|
||||||
|
() => props.packageRow?.real_total_mb ?? data.value?.total_usage_mb ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
// 监听对话框打开,加载数据
|
// 监听对话框打开,加载数据
|
||||||
watch(visible, async (newVal) => {
|
watch(visible, async (newVal) => {
|
||||||
@@ -168,7 +185,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredRecords.value = data.value.records.filter((record: any) => {
|
filteredRecords.value = data.value.records.filter((record) => {
|
||||||
return record.date === selectedDate.value
|
return record.date === selectedDate.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -181,7 +198,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredRecords.value = data.value.records.filter((record: any) => {
|
filteredRecords.value = data.value.records.filter((record) => {
|
||||||
return record.date.startsWith(selectedMonth.value)
|
return record.date.startsWith(selectedMonth.value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ type TrafficDisplaySource = {
|
|||||||
enable_virtual_data?: boolean | null
|
enable_virtual_data?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NULLABLE_REALTIME_CARD_FIELDS = new Set([
|
||||||
|
'last_data_check_at',
|
||||||
|
'last_real_name_check_at',
|
||||||
|
'last_card_status_check_at'
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化数据大小(MB -> GB/MB)
|
* 格式化数据大小(MB -> GB/MB)
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +60,7 @@ const mergeAssetBoundCards = (
|
|||||||
|
|
||||||
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
const mergedCard = { ...existingCard } as unknown as Record<string, unknown>
|
||||||
for (const [key, value] of Object.entries(realtimeCard)) {
|
for (const [key, value] of Object.entries(realtimeCard)) {
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== undefined && (value !== null || NULLABLE_REALTIME_CARD_FIELDS.has(key))) {
|
||||||
mergedCard[key] = value
|
mergedCard[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,6 +310,15 @@ export function useAssetInfo() {
|
|||||||
if (data.last_sync_time !== undefined) {
|
if (data.last_sync_time !== undefined) {
|
||||||
cardInfo.value.last_sync_time = data.last_sync_time
|
cardInfo.value.last_sync_time = data.last_sync_time
|
||||||
}
|
}
|
||||||
|
if (data.last_data_check_at !== undefined) {
|
||||||
|
cardInfo.value.last_data_check_at = data.last_data_check_at
|
||||||
|
}
|
||||||
|
if (data.last_real_name_check_at !== undefined) {
|
||||||
|
cardInfo.value.last_real_name_check_at = data.last_real_name_check_at
|
||||||
|
}
|
||||||
|
if (data.last_card_status_check_at !== undefined) {
|
||||||
|
cardInfo.value.last_card_status_check_at = data.last_card_status_check_at
|
||||||
|
}
|
||||||
if (data.current_month_usage_mb !== undefined) {
|
if (data.current_month_usage_mb !== undefined) {
|
||||||
cardInfo.value.current_month_usage_mb = data.current_month_usage_mb
|
cardInfo.value.current_month_usage_mb = data.current_month_usage_mb
|
||||||
}
|
}
|
||||||
@@ -320,6 +335,15 @@ export function useAssetInfo() {
|
|||||||
// 仅用实时接口中非 null/undefined 的字段覆盖,避免清掉 resolve 接口返回的静态数据
|
// 仅用实时接口中非 null/undefined 的字段覆盖,避免清掉 resolve 接口返回的静态数据
|
||||||
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
cardInfo.value.cards = mergeAssetBoundCards(cardInfo.value.cards || [], data.cards)
|
||||||
}
|
}
|
||||||
|
if (data.last_data_check_at !== undefined) {
|
||||||
|
cardInfo.value.last_data_check_at = data.last_data_check_at
|
||||||
|
}
|
||||||
|
if (data.last_real_name_check_at !== undefined) {
|
||||||
|
cardInfo.value.last_real_name_check_at = data.last_real_name_check_at
|
||||||
|
}
|
||||||
|
if (data.last_card_status_check_at !== undefined) {
|
||||||
|
cardInfo.value.last_card_status_check_at = data.last_card_status_check_at
|
||||||
|
}
|
||||||
if (data.online_status !== undefined) {
|
if (data.online_status !== undefined) {
|
||||||
cardInfo.value.online_status = data.online_status
|
cardInfo.value.online_status = data.online_status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
<DailyRecordsDialog
|
<DailyRecordsDialog
|
||||||
v-model="dailyRecordsDialogVisible"
|
v-model="dailyRecordsDialogVisible"
|
||||||
:package-usage-id="selectedPackageUsageId ?? undefined"
|
:package-usage-id="selectedPackageUsageId ?? undefined"
|
||||||
|
:package-row="selectedPackageRow ?? undefined"
|
||||||
/>
|
/>
|
||||||
<OrderHistoryDialog
|
<OrderHistoryDialog
|
||||||
v-model="orderHistoryDialogVisible"
|
v-model="orderHistoryDialogVisible"
|
||||||
@@ -196,7 +197,7 @@
|
|||||||
import { useAssetOperations } from './composables/useAssetOperations'
|
import { useAssetOperations } from './composables/useAssetOperations'
|
||||||
|
|
||||||
// 引入类型
|
// 引入类型
|
||||||
import type { BindingCard, SwitchCardForm, WiFiForm } from './types'
|
import type { BindingCard, PackageInfo, SwitchCardForm, WiFiForm } from './types'
|
||||||
|
|
||||||
defineOptions({ name: 'SingleCard' })
|
defineOptions({ name: 'SingleCard' })
|
||||||
|
|
||||||
@@ -264,6 +265,7 @@
|
|||||||
const realnamePolicyDialogVisible = ref(false)
|
const realnamePolicyDialogVisible = ref(false)
|
||||||
const updateRealnameStatusDialogVisible = ref(false)
|
const updateRealnameStatusDialogVisible = ref(false)
|
||||||
const selectedPackageUsageId = ref<number | null>(null)
|
const selectedPackageUsageId = ref<number | null>(null)
|
||||||
|
const selectedPackageRow = ref<PackageInfo | null>(null)
|
||||||
|
|
||||||
// 绑定卡实名状态更新
|
// 绑定卡实名状态更新
|
||||||
const bindingCardRealnameStatusDialogVisible = ref(false)
|
const bindingCardRealnameStatusDialogVisible = ref(false)
|
||||||
@@ -610,9 +612,10 @@
|
|||||||
/**
|
/**
|
||||||
* 显示流量详单
|
* 显示流量详单
|
||||||
*/
|
*/
|
||||||
const handleShowDailyRecords = (payload: { packageRow: any }) => {
|
const handleShowDailyRecords = (payload: { packageRow: PackageInfo }) => {
|
||||||
const { packageRow } = payload
|
const { packageRow } = payload
|
||||||
selectedPackageUsageId.value = packageRow.package_usage_id
|
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id
|
||||||
|
selectedPackageRow.value = packageRow
|
||||||
dailyRecordsDialogVisible.value = true
|
dailyRecordsDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export interface AssetInfo {
|
|||||||
cards?: BindingCard[]
|
cards?: BindingCard[]
|
||||||
status?: number
|
status?: number
|
||||||
gateway_extend?: string
|
gateway_extend?: string
|
||||||
|
last_sync_time?: string | null
|
||||||
|
last_data_check_at?: string | null
|
||||||
|
last_real_name_check_at?: string | null
|
||||||
|
last_card_status_check_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备绑定卡信息
|
// 设备绑定卡信息
|
||||||
@@ -45,6 +49,9 @@ export interface BindingCard {
|
|||||||
real_name_status?: number
|
real_name_status?: number
|
||||||
realname_policy?: string
|
realname_policy?: string
|
||||||
gateway_extend?: string
|
gateway_extend?: string
|
||||||
|
last_data_check_at?: string | null
|
||||||
|
last_real_name_check_at?: string | null
|
||||||
|
last_card_status_check_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备实时信息接口
|
// 设备实时信息接口
|
||||||
@@ -99,7 +106,9 @@ export interface DeviceRealtimeInfo {
|
|||||||
// 套餐信息接口
|
// 套餐信息接口
|
||||||
export interface PackageInfo {
|
export interface PackageInfo {
|
||||||
id: number
|
id: number
|
||||||
|
package_usage_id?: number
|
||||||
package_id: number
|
package_id: number
|
||||||
|
order_id?: number
|
||||||
package_name: string
|
package_name: string
|
||||||
package_price: number
|
package_price: number
|
||||||
paid_amount?: number
|
paid_amount?: number
|
||||||
@@ -114,6 +123,7 @@ export interface PackageInfo {
|
|||||||
status: number
|
status: number
|
||||||
status_name?: string
|
status_name?: string
|
||||||
duration_days?: number
|
duration_days?: number
|
||||||
|
data_usage_mb?: number
|
||||||
reduction_pct?: number // 展示增幅比例
|
reduction_pct?: number // 展示增幅比例
|
||||||
package_type?: string // 套餐类型: formal(正式套餐)/ addon(加油包)
|
package_type?: string // 套餐类型: formal(正式套餐)/ addon(加油包)
|
||||||
enable_virtual_data?: boolean // 是否启用虚流量
|
enable_virtual_data?: boolean // 是否启用虚流量
|
||||||
|
|||||||
@@ -243,6 +243,15 @@
|
|||||||
<ElInput v-model="seriesBindingForm.virtual_no_end" placeholder="请输入结束设备号" />
|
<ElInput v-model="seriesBindingForm.virtual_no_end" placeholder="请输入结束设备号" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
seriesBindingForm.selection_type === DeviceSelectionType.RANGE &&
|
||||||
|
seriesBindingRangeCountText
|
||||||
|
"
|
||||||
|
class="range-allocation-hint range-allocation-hint--device"
|
||||||
|
>
|
||||||
|
共分配{{ seriesBindingRangeCountText }}台设备
|
||||||
|
</div>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
|
||||||
label="设备号"
|
label="设备号"
|
||||||
@@ -666,7 +675,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import {
|
import {
|
||||||
@@ -814,6 +823,65 @@
|
|||||||
created_at_end: ''
|
created_at_end: ''
|
||||||
})
|
})
|
||||||
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
||||||
|
const parseNumericStringToBigInt = (value: string): bigint | null => {
|
||||||
|
const normalizedValue = value.trim()
|
||||||
|
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return BigInt(normalizedValue)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getPositiveRangeCountText = (start: bigint | null, end: bigint | null): string => {
|
||||||
|
if (start === null || end === null || end < start) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return (end - start + 1n).toString()
|
||||||
|
}
|
||||||
|
const parseDeviceRangeEndpoint = (value: string): { prefix: string; number: bigint } | null => {
|
||||||
|
const normalizedValue = value.trim()
|
||||||
|
if (!normalizedValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = parseNumericStringToBigInt(normalizedValue)
|
||||||
|
if (numericValue !== null) {
|
||||||
|
return {
|
||||||
|
prefix: '',
|
||||||
|
number: numericValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = normalizedValue.match(/^(.*?)(\d+)$/)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
prefix: match[1],
|
||||||
|
number: BigInt(match[2])
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getDeviceRangeCountText = (startValue: string, endValue: string): string => {
|
||||||
|
const start = parseDeviceRangeEndpoint(startValue)
|
||||||
|
const end = parseDeviceRangeEndpoint(endValue)
|
||||||
|
if (!start || !end || start.prefix !== end.prefix) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPositiveRangeCountText(start.number, end.number)
|
||||||
|
}
|
||||||
|
const seriesBindingRangeCountText = computed(() =>
|
||||||
|
getDeviceRangeCountText(seriesBindingForm.virtual_no_start, seriesBindingForm.virtual_no_end)
|
||||||
|
)
|
||||||
const seriesBindingRules = reactive<FormRules>({
|
const seriesBindingRules = reactive<FormRules>({
|
||||||
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }],
|
selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }],
|
||||||
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||||
@@ -2389,6 +2457,16 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.range-allocation-hint {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-allocation-hint--device {
|
||||||
|
margin: -6px 0 18px 120px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-table__row.table-row-with-context-menu) {
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,6 +379,15 @@
|
|||||||
>
|
>
|
||||||
<ElInput v-model="seriesBindingForm.iccid_end" placeholder="请输入结束ICCID" />
|
<ElInput v-model="seriesBindingForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
seriesBindingForm.selection_type === CardSelectionType.RANGE &&
|
||||||
|
seriesBindingRangeCountText
|
||||||
|
"
|
||||||
|
class="range-allocation-hint range-allocation-hint--card"
|
||||||
|
>
|
||||||
|
共分配{{ seriesBindingRangeCountText }}张卡
|
||||||
|
</div>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
|
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
|
||||||
label="运营商"
|
label="运营商"
|
||||||
@@ -608,7 +617,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { RoutesAlias } from '@/router/routesAlias'
|
import { RoutesAlias } from '@/router/routesAlias'
|
||||||
import {
|
import {
|
||||||
@@ -689,6 +698,31 @@
|
|||||||
batch_no: ''
|
batch_no: ''
|
||||||
})
|
})
|
||||||
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
const seriesBindingForm = reactive(createInitialSeriesBindingState())
|
||||||
|
const parseNumericStringToBigInt = (value: string): bigint | null => {
|
||||||
|
const normalizedValue = value.trim()
|
||||||
|
if (!normalizedValue || !/^\d+$/.test(normalizedValue)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return BigInt(normalizedValue)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getPositiveRangeCountText = (start: bigint | null, end: bigint | null): string => {
|
||||||
|
if (start === null || end === null || end < start) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return (end - start + 1n).toString()
|
||||||
|
}
|
||||||
|
const seriesBindingRangeCountText = computed(() =>
|
||||||
|
getPositiveRangeCountText(
|
||||||
|
parseNumericStringToBigInt(seriesBindingForm.iccid_start),
|
||||||
|
parseNumericStringToBigInt(seriesBindingForm.iccid_end)
|
||||||
|
)
|
||||||
|
)
|
||||||
const seriesBindingRules = reactive<FormRules>({
|
const seriesBindingRules = reactive<FormRules>({
|
||||||
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
|
selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
|
||||||
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||||
@@ -2149,6 +2183,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.range-allocation-hint {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-allocation-hint--card {
|
||||||
|
margin: -6px 0 18px 100px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-table__row.table-row-with-context-menu) {
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,15 +60,14 @@
|
|||||||
<ElFormItem label="充值金额" prop="amount">
|
<ElFormItem label="充值金额" prop="amount">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="createForm.amount"
|
v-model="createForm.amount"
|
||||||
:min="100"
|
:min="0.01"
|
||||||
:max="1000000"
|
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:step="100"
|
:step="0.01"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="请输入充值金额(元)"
|
placeholder="请输入充值金额(元)"
|
||||||
/>
|
/>
|
||||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||||
充值范围: ¥100 ~ ¥1,000,000
|
最小: ¥0.01,最大: 不限制
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="支付方式" prop="payment_method">
|
<ElFormItem label="支付方式" prop="payment_method">
|
||||||
@@ -337,10 +336,27 @@
|
|||||||
const createFormRef = ref<FormInstance>()
|
const createFormRef = ref<FormInstance>()
|
||||||
const confirmPayFormRef = ref<FormInstance>()
|
const confirmPayFormRef = ref<FormInstance>()
|
||||||
const uploadRef = ref()
|
const uploadRef = ref()
|
||||||
|
const MIN_RECHARGE_AMOUNT = 0.01
|
||||||
|
|
||||||
const createRules = computed<FormRules>(() => {
|
const createRules = computed<FormRules>(() => {
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
amount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }],
|
amount: [
|
||||||
|
{ required: true, message: '请输入充值金额', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
if (value == null || value === '') {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Number(value) < MIN_RECHARGE_AMOUNT) {
|
||||||
|
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||||
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
|
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
|
||||||
}
|
}
|
||||||
@@ -362,7 +378,7 @@
|
|||||||
shop_id: number | null
|
shop_id: number | null
|
||||||
payment_voucher_key?: string
|
payment_voucher_key?: string
|
||||||
}>({
|
}>({
|
||||||
amount: 100,
|
amount: MIN_RECHARGE_AMOUNT,
|
||||||
payment_method: '',
|
payment_method: '',
|
||||||
shop_id: null,
|
shop_id: null,
|
||||||
payment_voucher_key: undefined
|
payment_voucher_key: undefined
|
||||||
@@ -646,7 +662,7 @@
|
|||||||
// 对话框关闭后的清理
|
// 对话框关闭后的清理
|
||||||
const handleCreateDialogClosed = () => {
|
const handleCreateDialogClosed = () => {
|
||||||
createFormRef.value?.resetFields()
|
createFormRef.value?.resetFields()
|
||||||
createForm.amount = 100
|
createForm.amount = MIN_RECHARGE_AMOUNT
|
||||||
createForm.payment_method = ''
|
createForm.payment_method = ''
|
||||||
createForm.shop_id = null
|
createForm.shop_id = null
|
||||||
createForm.payment_voucher_key = undefined
|
createForm.payment_voucher_key = undefined
|
||||||
@@ -702,14 +718,15 @@
|
|||||||
|
|
||||||
// 创建充值订单
|
// 创建充值订单
|
||||||
const handleCreateRecharge = async () => {
|
const handleCreateRecharge = async () => {
|
||||||
if (!createFormRef.value) return
|
const formRef = createFormRef.value
|
||||||
|
if (!formRef) return
|
||||||
|
|
||||||
await createFormRef.value.validate(async (valid) => {
|
await formRef.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
createLoading.value = true
|
createLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data: CreateAgentRechargeRequest = {
|
const data: CreateAgentRechargeRequest = {
|
||||||
amount: createForm.amount * 100, // 元转分
|
amount: Math.round(createForm.amount * 100), // 元转分
|
||||||
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
|
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
|
||||||
shop_id: createForm.shop_id!
|
shop_id: createForm.shop_id!
|
||||||
}
|
}
|
||||||
@@ -721,7 +738,7 @@
|
|||||||
await AgentRechargeService.createAgentRecharge(data)
|
await AgentRechargeService.createAgentRecharge(data)
|
||||||
ElMessage.success('充值订单创建成功')
|
ElMessage.success('充值订单创建成功')
|
||||||
createDialogVisible.value = false
|
createDialogVisible.value = false
|
||||||
createFormRef.value.resetFields()
|
formRef.resetFields()
|
||||||
await getTableData()
|
await getTableData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -747,18 +764,20 @@
|
|||||||
|
|
||||||
// 确认线下支付
|
// 确认线下支付
|
||||||
const handleConfirmPay = async () => {
|
const handleConfirmPay = async () => {
|
||||||
if (!confirmPayFormRef.value || !currentRecharge.value) return
|
const formRef = confirmPayFormRef.value
|
||||||
|
const recharge = currentRecharge.value
|
||||||
|
if (!formRef || !recharge) return
|
||||||
|
|
||||||
await confirmPayFormRef.value.validate(async (valid) => {
|
await formRef.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
confirmPayLoading.value = true
|
confirmPayLoading.value = true
|
||||||
try {
|
try {
|
||||||
await AgentRechargeService.confirmOfflinePayment(currentRecharge.value.id, {
|
await AgentRechargeService.confirmOfflinePayment(recharge.id, {
|
||||||
operation_password: confirmPayForm.operation_password
|
operation_password: confirmPayForm.operation_password
|
||||||
})
|
})
|
||||||
ElMessage.success('确认支付成功')
|
ElMessage.success('确认支付成功')
|
||||||
confirmPayDialogVisible.value = false
|
confirmPayDialogVisible.value = false
|
||||||
confirmPayFormRef.value.resetFields()
|
formRef.resetFields()
|
||||||
await getTableData()
|
await getTableData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
Reference in New Issue
Block a user