feat: 卡和设备实名状态筛选
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m40s

This commit is contained in:
luo
2026-07-22 15:44:48 +08:00
parent 68aa03b538
commit 8de4339505
10 changed files with 281 additions and 7 deletions

View File

@@ -0,0 +1,37 @@
## Context
卡和设备列表需要新增统一的实名状态筛选与展示能力。设备的实名状态由设备列表接口直接返回,不能通过关联卡在前端二次计算,以避免多卡设备或卡绑定关系变化时出现不一致。
## Goals / Non-Goals
- Goals:
- 支持按“全部”“已实名”“未实名”筛选卡和设备。
- 直接展示后端返回的实名状态名称。
- 在搜索、刷新、分页和导出查询中保留当前筛选条件。
- Non-Goals:
- 不新增或修改实名认证流程、策略配置或状态更新操作。
- 不在前端推导设备实名状态。
- 不变更其他资产详情页的实名状态取值规则。
## Decisions
- Decision: 使用可选数值查询参数 `real_name_status`,其中 `0` 表示未实名、`1` 表示已实名;“全部”对应不传该参数。
- Decision: 列表展示优先使用每条记录的 `real_name_status_name`,而非根据 `real_name_status` 写死文案。
- Decision: 设备列表把接口响应中的 `real_name_status``real_name_status_name` 作为唯一状态来源,不读取或遍历绑定卡数据。
- Alternatives considered: 前端将 `0``1` 映射为固定文案。未采用,因为后端已提供标准显示名称,直接使用可避免展示口径分叉。
## Risks / Trade-offs
- 后端未返回 `real_name_status_name` 时无法满足状态名称展示契约 -> 联调时校验列表响应字段,并将该字段设为必需的列表类型字段。
- 卡列表当前存在不同的查询实现 -> 实施时确认并统一至本提案指定的 `GET /api/admin/iot-cards` 契约,避免筛选参数发往不支持的接口。
## Migration Plan
1. 扩展卡与设备列表 API 类型和查询参数。
2. 接入筛选控件、查询参数及状态列。
3. 验证全部、已实名、未实名筛选以及分页和重置行为。
4. 如需回滚,移除前端筛选控件、查询参数和状态列;不涉及数据迁移。
## Open Questions
- 无。

View File

@@ -0,0 +1,33 @@
# Change: 新增卡和设备实名状态筛选
## Why
运营人员需要在卡列表和设备列表中快速识别并筛选已实名或未实名的资产。当前页面未完整接入实名状态筛选和后端返回的实名状态名称,设备列表尤其不能依赖前端遍历绑定卡来推导状态。
## What Changes
- 卡列表和设备列表的筛选区新增“实名状态”,提供“全部”“已实名”“未实名”选项。
- 卡列表查询使用 `GET /api/admin/iot-cards`,设备列表查询使用 `GET /api/admin/devices`;选择状态后分别传递 `real_name_status=0|1`,未选择时不传该参数。
- 卡和设备列表项的类型契约支持 `real_name_status: int``real_name_status_name: string`
- 两个列表表格新增“实名状态”列,直接展示接口返回的 `real_name_status_name`
- 设备列表直接使用设备列表接口的实名状态字段,不遍历或关联绑定卡计算设备实名状态。
- 搜索、刷新和分页切换必须保留当前实名状态筛选;重置搜索时清空该筛选。
## Impact
- Affected specs:
- `iot-card-management`
- `device-management`
- Affected code:
- `src/api/modules/card.ts`
- `src/api/modules/device.ts`
- `src/types/api/card.ts`
- `src/types/api/device.ts`
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/asset-management/device-list/index.vue`
- API contracts:
- `GET /api/admin/iot-cards?real_name_status=0|1`
- `GET /api/admin/devices?real_name_status=0|1`
- List items return `real_name_status: int` and `real_name_status_name: string`
- Dependencies:
- 后端列表接口必须支持实名状态查询并返回实名状态名称。

View File

@@ -0,0 +1,68 @@
## ADDED Requirements
### Requirement: Device Realname Status Query Contract
The device management list integration SHALL support an optional numeric `real_name_status` parameter on `GET /api/admin/devices`. The list-item contract SHALL include `real_name_status` and `real_name_status_name` returned by the device list API.
#### Scenario: Query devices by realname status
- **GIVEN** 用户正在后台设备列表使用实名状态筛选
- **WHEN** 用户选择“已实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/devices` with `real_name_status=1`
#### Scenario: Query unverified devices
- **GIVEN** 用户正在后台设备列表使用实名状态筛选
- **WHEN** 用户选择“未实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/devices` with `real_name_status=0`
- **AND** 前端 MUST NOT 因为该值为 `0` 而省略此参数
#### Scenario: Query all devices without status restriction
- **GIVEN** 用户未选择实名状态或选择“全部”
- **WHEN** 用户查询、刷新或切换设备列表分页
- **THEN** 请求 MUST NOT 携带 `real_name_status`
#### Scenario: Receive device realname status fields
- **GIVEN** 设备列表接口返回设备记录
- **WHEN** 前端解析列表响应
- **THEN** 列表项类型 MUST 支持 `real_name_status: int`
- **AND** 列表项类型 MUST 支持 `real_name_status_name: string`
### Requirement: Device Realname Status Filter And Display
The device management page SHALL provide a `实名状态` filter with `全部``已实名``未实名` options and display the backend device realname status name in the device table.
#### Scenario: Display device realname status filter
- **GIVEN** 用户打开后台设备列表
- **WHEN** 页面渲染筛选区
- **THEN** 页面 MUST display a `实名状态` filter
- **AND** 筛选项 MUST provide `全部``已实名``未实名` options
#### Scenario: Display backend device realname status name
- **GIVEN** 设备列表接口返回某条记录的 `real_name_status_name`
- **WHEN** 页面渲染该设备的表格行
- **THEN** 页面 MUST 在“实名状态”列显示该字段值
#### Scenario: Use the device API as the status source
- **GIVEN** 设备列表接口返回设备的实名状态字段
- **WHEN** 页面渲染设备实名状态
- **THEN** 页面 MUST directly use the record's `real_name_status` and `real_name_status_name`
- **AND** 页面 MUST NOT 遍历、绑定或计算关联卡的实名状态
#### Scenario: Preserve device realname status while paginating
- **GIVEN** 用户已选择“已实名”或“未实名”并获得筛选结果
- **WHEN** 用户切换设备列表页码或每页条数
- **THEN** 后续列表请求 MUST 保留当前的 `real_name_status` 参数
#### Scenario: Reset device realname status filter
- **GIVEN** 用户已选择实名状态
- **WHEN** 用户重置设备列表搜索条件
- **THEN** 页面 MUST 清空实名状态筛选
- **AND** 后续列表请求 MUST NOT 携带 `real_name_status`

View File

@@ -0,0 +1,61 @@
## ADDED Requirements
### Requirement: IoT Card Realname Status Query Contract
The IoT card management list integration SHALL query `GET /api/admin/iot-cards` and support an optional numeric `real_name_status` parameter. The list-item contract SHALL include `real_name_status` and `real_name_status_name`.
#### Scenario: Query cards by realname status
- **GIVEN** 用户正在后台卡列表使用实名状态筛选
- **WHEN** 用户选择“已实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/iot-cards` with `real_name_status=1`
#### Scenario: Query unverified cards
- **GIVEN** 用户正在后台卡列表使用实名状态筛选
- **WHEN** 用户选择“未实名”并执行搜索
- **THEN** 系统 MUST call `GET /api/admin/iot-cards` with `real_name_status=0`
- **AND** 前端 MUST NOT 因为该值为 `0` 而省略此参数
#### Scenario: Query all cards without status restriction
- **GIVEN** 用户未选择实名状态或选择“全部”
- **WHEN** 用户查询、刷新或切换卡列表分页
- **THEN** 请求 MUST NOT 携带 `real_name_status`
#### Scenario: Receive card realname status fields
- **GIVEN** 卡列表接口返回资产记录
- **WHEN** 前端解析列表响应
- **THEN** 列表项类型 MUST 支持 `real_name_status: int`
- **AND** 列表项类型 MUST 支持 `real_name_status_name: string`
### Requirement: IoT Card Realname Status Filter And Display
The IoT card management page SHALL provide a `实名状态` filter with `全部``已实名``未实名` options and display the backend realname status name in the card table.
#### Scenario: Display card realname status filter
- **GIVEN** 用户打开后台卡列表
- **WHEN** 页面渲染筛选区
- **THEN** 页面 MUST display a `实名状态` filter
- **AND** 筛选项 MUST provide `全部``已实名``未实名` options
#### Scenario: Display backend card realname status name
- **GIVEN** 卡列表接口返回某条记录的 `real_name_status_name`
- **WHEN** 页面渲染该卡的表格行
- **THEN** 页面 MUST 在“实名状态”列显示该字段值
#### Scenario: Preserve card realname status while paginating
- **GIVEN** 用户已选择“已实名”或“未实名”并获得筛选结果
- **WHEN** 用户切换卡列表页码或每页条数
- **THEN** 后续列表请求 MUST 保留当前的 `real_name_status` 参数
#### Scenario: Reset card realname status filter
- **GIVEN** 用户已选择实名状态
- **WHEN** 用户重置卡列表搜索条件
- **THEN** 页面 MUST 清空实名状态筛选
- **AND** 后续列表请求 MUST NOT 携带 `real_name_status`

View File

@@ -0,0 +1,26 @@
## 1. API Contracts And Types
- [ ] 1.1 扩展卡列表查询参数和列表项类型,支持可选 `real_name_status` 以及必需的 `real_name_status_name`
- [ ] 1.2 扩展设备列表查询参数和列表项类型,支持可选 `real_name_status` 以及必需的 `real_name_status_name`
- [ ] 1.3 将卡列表查询接入 `GET /api/admin/iot-cards` 并传递实名状态筛选参数。
## 2. Card List
- [ ] 2.1 在卡列表筛选区新增“实名状态”的全部、已实名、未实名选项。
- [ ] 2.2 将选中的实名状态传递给卡列表查询,并在搜索、刷新、分页、导出中保留该条件。
- [ ] 2.3 在卡列表表格新增“实名状态”列,展示接口返回的 `real_name_status_name`
- [ ] 2.4 重置卡列表搜索时清空实名状态筛选。
## 3. Device List
- [ ] 3.1 在设备列表筛选区新增“实名状态”的全部、已实名、未实名选项。
- [ ] 3.2 将选中的实名状态传递给 `GET /api/admin/devices`,并在搜索、刷新、分页、导出中保留该条件。
- [ ] 3.3 在设备列表表格新增“实名状态”列,直接展示接口返回的 `real_name_status_name`,不遍历绑定卡计算状态。
- [ ] 3.4 重置设备列表搜索时清空实名状态筛选。
## 4. Verification
- [ ] 4.1 验证卡列表“全部”“已实名”“未实名”分别不传、传 `1`、传 `0`,且状态列显示接口名称。
- [ ] 4.2 验证设备列表“全部”“已实名”“未实名”分别不传、传 `1`、传 `0`,且状态列直接显示设备接口名称。
- [ ] 4.3 验证两个列表在分页切换、刷新和导出时保留实名状态筛选,重置后清空该条件。
- [ ] 4.4 运行相关类型检查、lint 或构建验证。

View File

@@ -349,6 +349,16 @@ export class CardService extends BaseService {
return this.getPage<StandaloneIotCard>('/api/admin/iot-cards/standalone', params)
}
/**
* 获取 IoT 卡列表
* @param params 查询参数
*/
static getIotCards(
params?: StandaloneCardQueryParams
): Promise<PaginationResponse<StandaloneIotCard>> {
return this.getPage<StandaloneIotCard>('/api/admin/iot-cards', params)
}
/**
* 批量分配单卡
* @param data 分配参数

View File

@@ -346,6 +346,7 @@ export enum StandaloneCardStatus {
export interface StandaloneCardQueryParams extends PaginationParams {
keyword?: string // 通用关键字搜索
has_active_package?: boolean // has active package filter
real_name_status?: 0 | 1 // 实名状态0 未实名 / 1 已实名
carrier_name?: string // carrier name filter
is_standalone?: boolean // standalone filter
status?: StandaloneCardStatus // 状态
@@ -384,7 +385,8 @@ export interface StandaloneIotCard {
status: StandaloneCardStatus // 状态
activation_status: number // 激活状态 (0:未激活, 1:已激活)
network_status: number // 网络状态 (0:停机, 1:开机)
real_name_status: number // 实名状态 (0:未实名, 1:已实名)
real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名)
real_name_status_name: string // 实名状态名称
generation?: number // 资产世代编号初始值1每次换货转新后+1
asset_status?: AssetStatus // 业务状态 (1:在库, 2:已销售, 3:已换货, 4:已停用)
asset_status_name?: string // 业务状态名称(中文)

View File

@@ -65,6 +65,8 @@ export interface Device {
imei?: string // IMEI设备国际移动设备识别码
sn?: string // 设备序列号
realname_policy?: string // 实名认证策略
real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名)
real_name_status_name: string // 实名状态名称
authorized_enterprise_id?: number | null // 当前有效授权企业ID
authorized_enterprise_name?: string | null // 当前有效授权企业名称
}
@@ -72,6 +74,7 @@ export interface Device {
// 设备查询参数
export interface DeviceQueryParams extends PaginationParams {
has_active_package?: boolean // has active package filter
real_name_status?: 0 | 1 // 实名状态0 未实名 / 1 已实名
virtual_no?: string // 虚拟号(模糊查询,原 device_no)
device_name?: string // 设备名称(模糊查询)
status?: DeviceStatus // 状态

View File

@@ -1337,6 +1337,7 @@
device_name: '',
status: undefined as DeviceStatus | undefined,
activation_status: undefined as number | undefined,
real_name_status: undefined as 0 | 1 | undefined,
batch_no: '',
device_type: '',
manufacturer: '',
@@ -1447,6 +1448,19 @@
{ label: '已激活', value: 1 }
]
},
{
label: '实名状态',
prop: 'real_name_status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已实名', value: 1 },
{ label: '未实名', value: 0 }
]
},
{
label: '批次号',
prop: 'batch_no',
@@ -1599,6 +1613,7 @@
{ label: '资产世代', prop: 'generation' },
{ label: '业务状态', prop: 'asset_status' },
{ label: '激活状态', prop: 'activation_status_name' },
{ label: '实名状态', prop: 'real_name_status_name' },
{ label: '在线状态', prop: 'online_status' },
{ label: '实名策略', prop: 'realname_policy' },
{ label: '切卡模式', prop: 'switch_mode' },
@@ -1934,6 +1949,12 @@
width: 120,
formatter: (row: Device) => row.activation_status_name || '-'
},
{
prop: 'real_name_status_name',
label: '实名状态',
width: 100,
formatter: (row: Device) => row.real_name_status_name || '-'
},
{
prop: 'online_status',
label: '在线状态',
@@ -2126,6 +2147,7 @@
device_name: searchForm.device_name || undefined,
status: searchForm.status,
activation_status: searchForm.activation_status ?? undefined,
real_name_status: searchForm.real_name_status ?? undefined,
batch_no: searchForm.batch_no || undefined,
device_type: searchForm.device_type || undefined,
manufacturer: searchForm.manufacturer || undefined,
@@ -2156,6 +2178,7 @@
device_name: searchForm.device_name || undefined,
status: searchForm.status,
activation_status: searchForm.activation_status ?? undefined,
real_name_status: searchForm.real_name_status ?? undefined,
batch_no: searchForm.batch_no || undefined,
device_type: searchForm.device_type || undefined,
manufacturer: searchForm.manufacturer || undefined,

View File

@@ -1556,6 +1556,7 @@
const initialSearchState: {
status: undefined | number
network_status: undefined | number
real_name_status: undefined | 0 | 1
iccid: string
carrier_name: string
msisdn: string
@@ -1572,6 +1573,7 @@
} = {
status: undefined,
network_status: undefined,
real_name_status: undefined,
iccid: '',
carrier_name: '',
msisdn: '',
@@ -1743,6 +1745,19 @@
{ label: '正常', value: 1 }
]
},
{
label: '实名状态',
prop: 'real_name_status',
type: 'select',
config: {
clearable: true,
placeholder: '全部'
},
options: () => [
{ label: '已实名', value: 1 },
{ label: '未实名', value: 0 }
]
},
{
label: '卡虚拟号',
prop: 'virtual_no',
@@ -2156,11 +2171,7 @@
prop: 'real_name_status',
label: '实名状态',
width: 100,
formatter: (row: StandaloneIotCard) => {
const type = row.real_name_status === 1 ? 'success' : 'warning'
const text = row.real_name_status === 1 ? '已实名' : '未实名'
return h(ElTag, { type }, () => text)
}
formatter: (row: StandaloneIotCard) => row.real_name_status_name || '-'
},
{
prop: 'realname_policy',
@@ -2338,7 +2349,7 @@
}
})
const res = await CardService.getStandaloneIotCards(params)
const res = await CardService.getIotCards(params)
if (res.code === 0) {
cardList.value = res.data.items || []
pagination.total = res.data.total || 0