feat: 换货新旧资产展示与独立搜索
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m34s

This commit is contained in:
luo
2026-07-22 16:57:17 +08:00
parent 8faa2ea2ef
commit 7d3352038a
5 changed files with 197 additions and 13 deletions

View File

@@ -0,0 +1,29 @@
# Change: 更新换货列表的新旧资产展示与独立搜索
## Why
换货管理列表当前使用一个资产标识符条件同时匹配旧资产和新资产,且表格未完整区分两侧资产类型与标识。运营人员无法准确按换出资产或换入资产检索、识别换货单。
## What Changes
- 将换货列表筛选区的单一“资产标识符”替换为“旧资产”和“新资产”两个独立输入框。
- 列表查询使用 `old_asset_keyword``new_asset_keyword`;两个条件可独立或组合传递,空条件不传。
- 停止在换货列表查询中使用旧的通用 `identifier` 参数。
- 扩展换货列表项契约,支持旧资产和新资产各自的 `type``id``identifier`,以及 `status``status_name`
- 换货列表分别展示旧资产类型、旧资产标识、新资产类型、新资产标识四列,不能混列。
- 物联网卡使用后端返回的 ICCID设备使用后端返回的设备号前端不转换接入号、虚拟号或其他输入标识。
## Impact
- Affected specs:
- `exchange-management`
- Related active changes:
- `update-exchange-flow-direct-type`,本提案替换其换货列表通用资产标识符筛选口径,不改变流程类型筛选与换货操作。
- `update-asset-exchange-generation-links`,仅共享换货领域概念,不修改资产详情换货链路展示。
- Affected code:
- `src/api/modules/exchange.ts`
- `src/views/asset-management/exchange-management/index.vue`
- API contract:
- `GET /api/admin/exchanges?old_asset_keyword=&new_asset_keyword=`
- Dependencies:
- 后端列表接口支持分别按旧资产和新资产关键词查询,并返回新旧资产字段。

View File

@@ -0,0 +1,96 @@
## ADDED Requirements
### Requirement: Exchange List Separate Asset Query Contract
The exchange management list integration SHALL query `GET /api/admin/exchanges` with optional `old_asset_keyword` and `new_asset_keyword` parameters instead of the deprecated generic `identifier` parameter.
#### Scenario: Query by old asset only
- **GIVEN** 用户在换货管理列表的“旧资产”输入框中填写关键词
- **AND** “新资产”输入框为空
- **WHEN** 用户执行搜索
- **THEN** 请求 MUST include `old_asset_keyword` with the entered value
- **AND** 请求 MUST NOT include `new_asset_keyword`
- **AND** 请求 MUST NOT include `identifier`
#### Scenario: Query by new asset only
- **GIVEN** 用户在换货管理列表的“新资产”输入框中填写关键词
- **AND** “旧资产”输入框为空
- **WHEN** 用户执行搜索
- **THEN** 请求 MUST include `new_asset_keyword` with the entered value
- **AND** 请求 MUST NOT include `old_asset_keyword`
- **AND** 请求 MUST NOT include `identifier`
#### Scenario: Combine old and new asset keywords
- **GIVEN** 用户在“旧资产”和“新资产”输入框中均填写关键词
- **WHEN** 用户执行搜索
- **THEN** 请求 MUST include both `old_asset_keyword` and `new_asset_keyword`
- **AND** 后端返回同时匹配两项条件的换货单
#### Scenario: Omit empty asset keywords
- **GIVEN** “旧资产”和“新资产”输入框均为空
- **WHEN** 用户查询、刷新、切换分页或重置搜索条件
- **THEN** 请求 MUST NOT include `old_asset_keyword`
- **AND** 请求 MUST NOT include `new_asset_keyword`
- **AND** 请求 MUST NOT include `identifier`
#### Scenario: Preserve raw asset keyword input
- **GIVEN** 用户输入 ICCID、接入号、虚拟号、IMEI 或 SN 作为旧资产或新资产关键词
- **WHEN** 页面发送列表查询请求
- **THEN** 页面 MUST pass the entered text unchanged in its corresponding keyword parameter
- **AND** 页面 MUST NOT convert an access number, virtual number, or other identifier into another asset identifier
### Requirement: Exchange List Separate Asset Display Contract
The exchange list-item contract SHALL preserve `old_asset_type`, `old_asset_id`, `old_asset_identifier`, `new_asset_type`, `new_asset_id`, `new_asset_identifier`, `status`, and `status_name` from each `GET /api/admin/exchanges` item.
#### Scenario: Parse old and new asset fields
- **GIVEN** 换货列表接口返回一条换货单
- **WHEN** 前端解析该列表项
- **THEN** 前端类型 MUST support the old asset type, ID, and identifier fields independently from the new asset fields
- **AND** 前端类型 MUST support `status` and `status_name`
### Requirement: Exchange List Separate Asset Columns
The exchange management list SHALL display distinct columns for `旧资产类型`, `旧资产标识`, `新资产类型`, and `新资产标识`.
#### Scenario: Display separate old and new IoT card assets
- **GIVEN** 换货列表项的 `old_asset_type``new_asset_type` 均为 `iot_card`
- **WHEN** 页面渲染换货列表行
- **THEN** 旧资产类型和新资产类型 MUST each display `物联网卡`
- **AND** 旧资产标识 MUST display `old_asset_identifier` as the backend-returned ICCID
- **AND** 新资产标识 MUST display `new_asset_identifier` as the backend-returned ICCID
#### Scenario: Display separate old and new device assets
- **GIVEN** 换货列表项的旧资产或新资产类型为 `device`
- **WHEN** 页面渲染换货列表行
- **THEN** 对应资产类型 MUST display `设备`
- **AND** 对应资产标识 MUST display its backend-returned device number
#### Scenario: Keep old and new asset values separated
- **GIVEN** 换货列表接口返回新旧资产字段
- **WHEN** 页面渲染换货列表行
- **THEN** 旧资产列 MUST only read `old_asset_type` and `old_asset_identifier`
- **AND** 新资产列 MUST only read `new_asset_type` and `new_asset_identifier`
- **AND** 页面 MUST NOT substitute a missing new asset value with an old asset value
#### Scenario: Display unavailable new asset placeholder
- **GIVEN** 换货列表项尚未关联新资产,或新资产类型或标识为空
- **WHEN** 页面渲染新资产列
- **THEN** 页面 MUST display a stable placeholder for each unavailable value
#### Scenario: Display backend exchange status name
- **GIVEN** 换货列表项返回 `status_name`
- **WHEN** 页面渲染状态列
- **THEN** 页面 MUST display `status_name`
- **AND** 当 `status_name` 缺失时页面 MUST display a stable placeholder

View File

@@ -0,0 +1,26 @@
## 1. API Contract And Types
- [ ] 1.1 将换货列表查询类型中的通用 `identifier` 替换为可选的 `old_asset_keyword``new_asset_keyword`
- [ ] 1.2 扩展换货列表项类型,支持 `old_asset_type``old_asset_id``old_asset_identifier``new_asset_type``new_asset_id``new_asset_identifier``status``status_name`
## 2. Exchange List Search
- [ ] 2.1 将换货管理列表筛选区的“资产标识符”替换为“旧资产”和“新资产”输入框。
- [ ] 2.2 查询、刷新和分页时保留并传递非空的 `old_asset_keyword``new_asset_keyword`,不再发送 `identifier`
- [ ] 2.3 重置搜索条件时清空新旧资产关键词,后续请求不发送这两个参数。
- [ ] 2.4 保留用户输入的 ICCID、接入号、虚拟号、IMEI 或 SN 原始值,不在前端转换后再传递给后端。
## 3. Exchange List Display
- [ ] 3.1 增加旧资产类型、旧资产标识、新资产类型、新资产标识四个独立列表列。
- [ ] 3.2 根据后端资产类型显示“物联网卡”或“设备”,并原样显示对应后端资产标识。
- [ ] 3.3 状态列优先展示后端 `status_name`,缺失时显示稳定占位内容。
- [ ] 3.4 新资产字段在未分配时显示稳定占位内容,不与旧资产字段混用。
## 4. Verification
- [ ] 4.1 验证仅输入旧资产、仅输入新资产、同时输入新旧资产时,请求分别传递正确的关键词参数。
- [ ] 4.2 验证两个资产关键词均为空时不传筛选参数,重置后保持相同行为。
- [ ] 4.3 验证旧资产和新资产类型、标识符分别显示IoT 卡显示 ICCID、设备显示设备号。
- [ ] 4.4 验证卡的 ICCID、接入号或虚拟号关键词均由后端按输入所在的旧/新资产条件命中,前端不转换输入值。
- [ ] 4.5 运行相关类型检查、lint 或构建验证。

View File

@@ -11,7 +11,8 @@ export interface ExchangeQueryParams {
page_size?: number
status?: number // 换货状态
flow_type?: string // 流程类型shipping/direct
identifier?: string // 资产标识符(模糊匹配,同时匹配旧资产、新资产
old_asset_keyword?: string // 资产关键词ICCID、接入号、虚拟号、IMEI、SN
new_asset_keyword?: string // 新资产关键词ICCID、接入号、虚拟号、IMEI、SN
created_at_start?: string // 创建时间起始
created_at_end?: string // 创建时间结束
}
@@ -33,10 +34,13 @@ export interface ExchangeResponse {
exchange_no: string
exchange_reason: string
old_asset_type: string
old_asset_id: number
old_asset_identifier: string
new_asset_type: string
new_asset_identifier: string
new_asset_type?: string | null
new_asset_id?: number | null
new_asset_identifier?: string | null
status: number // 换货状态1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消)
status_name?: string
status_text: string
flow_type: string // 流程类型shipping/direct
flow_type_name: string // 流程类型名称

View File

@@ -423,7 +423,8 @@
const searchForm = reactive({
status: undefined,
flow_type: undefined, // 流程类型筛选
identifier: '',
old_asset_keyword: '',
new_asset_keyword: '',
created_at_range: [],
created_at_start: '',
created_at_end: ''
@@ -626,11 +627,20 @@
]
},
{
label: '资产标识符',
prop: 'identifier',
label: '资产',
prop: 'old_asset_keyword',
type: 'input',
config: {
placeholder: '请输入资产标识',
placeholder: '请输入资产标识',
clearable: true
}
},
{
label: '新资产',
prop: 'new_asset_keyword',
type: 'input',
config: {
placeholder: '请输入新资产标识',
clearable: true
}
},
@@ -682,18 +692,25 @@
prop: 'old_asset_type',
label: '旧资产类型',
width: 120,
formatter: (row: any) => (row.old_asset_type === 'iot_card' ? 'IoT卡' : '设备')
formatter: (row: ExchangeResponse) => getAssetTypeName(row.old_asset_type)
},
{
prop: 'old_asset_identifier',
label: '旧资产标识符',
width: 220
width: 220,
formatter: (row: ExchangeResponse) => row.old_asset_identifier || '--'
},
{
prop: 'new_asset_type',
label: '新资产类型',
width: 120,
formatter: (row: ExchangeResponse) => getAssetTypeName(row.new_asset_type)
},
{
prop: 'new_asset_identifier',
label: '新资产标识符',
width: 220,
formatter: (row: any) => row.new_asset_identifier || '--'
formatter: (row: ExchangeResponse) => row.new_asset_identifier || '--'
},
{
prop: 'recipient_name',
@@ -734,7 +751,8 @@
prop: 'status',
label: '状态',
width: 130,
formatter: (row: any) => h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
formatter: (row: ExchangeResponse) =>
h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '--')
},
{
prop: 'created_at',
@@ -755,6 +773,12 @@
return types[status] || 'info'
}
const getAssetTypeName = (assetType?: string | null) => {
if (assetType === 'iot_card') return '物联网卡'
if (assetType === 'device') return '设备'
return '--'
}
// 加载换货单列表
const loadExchangeList = async () => {
loading.value = true
@@ -771,8 +795,11 @@
if (searchForm.flow_type) {
params.flow_type = searchForm.flow_type
}
if (searchForm.identifier) {
params.identifier = searchForm.identifier
if (searchForm.old_asset_keyword) {
params.old_asset_keyword = searchForm.old_asset_keyword
}
if (searchForm.new_asset_keyword) {
params.new_asset_keyword = searchForm.new_asset_keyword
}
if (searchForm.created_at_start) {
params.created_at_start = searchForm.created_at_start
@@ -809,6 +836,8 @@
// 重置
const handleReset = () => {
searchForm.old_asset_keyword = ''
searchForm.new_asset_keyword = ''
searchForm.created_at_range = []
searchForm.created_at_start = ''
searchForm.created_at_end = ''