From c31183205d19f71a4f666d9f130789a79cf635d7 Mon Sep 17 00:00:00 2001 From: luo Date: Tue, 13 Oct 2026 17:21:46 +0800 Subject: [PATCH] fix: some --- .../design.md | 4 + .../implementation.md | 13 +++ .../proposal.md | 3 +- .../specs/exchange-management/spec.md | 17 +++ .../spec.md | 34 ++++++ .../tasks.md | 7 ++ scripts/tests/octoberBatchOne.test.mjs | 85 ++++++++++++++ scripts/tests/octoberBrowserFixtures.mjs | 86 +++++++++++++- src/components/core/tables/ArtTable.vue | 3 + src/utils/business/exchangeError.ts | 23 ++++ src/utils/business/phoneAssetAssociation.ts | 30 +++++ .../exchange-management/index.vue | 31 +++-- .../phone-asset-association/index.vue | 109 ++++++++++++++++-- 13 files changed, 427 insertions(+), 18 deletions(-) create mode 100644 openspec/changes/update-october-batch-one-admin-integration/specs/phone-asset-association-management/spec.md create mode 100644 src/utils/business/phoneAssetAssociation.ts diff --git a/openspec/changes/update-october-batch-one-admin-integration/design.md b/openspec/changes/update-october-batch-one-admin-integration/design.md index f0d4def..209d033 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/design.md +++ b/openspec/changes/update-october-batch-one-admin-integration/design.md @@ -76,6 +76,10 @@ 创建失败保留资产/原因等字段;完成失败重新加载换货单,保持后端「已发货待确认」事实,不乐观标记完成。解除后由用户再次提交。取消只关闭提示,不当作解绑或换货成功。 +用户追加明确要求快捷定位并勾选关联:创建换货冻结当前新资产标识及关联手机号数组,确认完成使用原单新资产标识;错误响应手机号优先。主动跳转通过 `asset_identifier` 和 `phone` 查询参数传递目标,允许该 URL 带号码但不额外写日志或本地存储。关联页按 1 页/20 条加载,仅默认选中精确匹配的有效行;不自动解绑,继续原原因与二次确认。 + +通过 ArtTable 透传清空/勾选方法,数据更新后 nextTick 完成实际选择。缓存重入和路由参数变更重置查询,请求序号防止过期响应覆盖;手工筛选/翻页、解绑后刷新清除旧选择。批量结果保持后端布尔契约,仅展示中文「解绑成功/解绑失败」。原查看与批量解绑权限不变。 + ### 5. 预警只展示快照,不重算阈值 删除 `PackageTrafficAlertRuleItem.real_data_mb` 与规则行/详情/列设置依赖。来自 `/api/admin/packages` 的套餐选项 `real_data_mb` 保留,套餐选择与规则参数校验不变。 diff --git a/openspec/changes/update-october-batch-one-admin-integration/implementation.md b/openspec/changes/update-october-batch-one-admin-integration/implementation.md index cd116e3..f08788d 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/implementation.md +++ b/openspec/changes/update-october-batch-one-admin-integration/implementation.md @@ -51,3 +51,16 @@ node scripts/tests/verifyBuild.mjs --serve 4. 新建与历史订单、IoT 卡、设备、预警导出文件列。 这些项目继续保留在任务清单,用户验收及部署后再归档。 + +## 用户追加:换货冲突快捷解绑 + +已在用户明确要求下补充以下行为: + +- 创建换货捕获提交时新资产 ICCID/虚拟号及 `associated_phones`;确认完成使用换货单的新资产标识。跳转手机号优先取 1202 `data.phone`,缺失时取新资产手机号数组首个有效值,不使用旧资产。 +- 用户点击跳转后,路由带 `asset_identifier` 和 `phone`,目标页从 1 页/20 条精确查询。只实际勾选匹配两项条件且有效的记录,不全选历史失效记录。 +- 表格勾选方法由已有 ArtTable 透传;手工取消后不立即重新勾选,重置、筛选、翻页及解绑后刷新清空旧选择;缓存重入或路由参数变化重新取目标,过期响应不能覆盖新查询。 +- 批量解绑仍使用 `phone_asset_association:unbind_batch` 和原原因/二次确认表单;结果列展示「解绑成功/解绑失败」,资产类型同时显示中文,不修改后端布尔值契约。 + +原说明要求号码不额外记录或持久化;用户追加明确要求两项路由参数,因此用户主动跳转时允许号码进入该 URL 查询参数,不额外记录日志或写入本地存储。 + +新增三项自动测试,全套 66 项通过,类型检查、受影响文件 lint、编码和严格规范校验通过,生产构建通过(原有警告仍在)。本地模拟确认了完整的 1202 弹窗 → 带参跳转 → 筛选并勾选有效记录 → 手工取消禁用按钮 → 原确认表单 → 中文成功结果链路,没有调用真实后台解绑接口。 diff --git a/openspec/changes/update-october-batch-one-admin-integration/proposal.md b/openspec/changes/update-october-batch-one-admin-integration/proposal.md index d19d67f..357d14f 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/proposal.md +++ b/openspec/changes/update-october-batch-one-admin-integration/proposal.md @@ -13,6 +13,7 @@ - M8:核销申请列表和详情使用真实的 `approval_submission_status` 展示审批提交状态,不能只依赖当前可能为空的名称字段;账单详情中的申请状态同步扩展。 - M1:**BREAKING**:按用户最新确认,后台七个密码接口相关的既有表单、服务和类型统一到「8~12 位、只允许英文字母和数字,必须同时包含大写字母、小写字母和数字,不允许特殊符号」,包括后台 `PUT /api/auth/password`;账号独立改密服务按文档将请求字段修正为 `new_password`。旧密码登录、本人改密的旧密码输入、操作密码、WiFi 密码不套用新密码规则。 - M12:创建换货与确认完成捕获错误码 `1202`,从错误响应 `data.phone` 读取手机号,在弹窗中引导到已有「手机号资产关联」入口;不自动解绑、不新增扫码/退款前置拦截,失败时保留表单及原单状态。 +- 用户追加 M12 快捷处理:跳转携带本次新资产标识与关联手机号,关联页面自动预填筛选并勾选匹配有效记录;复用原批量解绑权限和二次确认,结果表布尔值改为中文成功/失败,不自动执行解绑。 - M16:**BREAKING**:删除预警规则响应类型、规则列表及规则详情展示中对 `real_data_mb` 的依赖;预警列表和详情补齐 `used_mb`、`limit_mb` 及新增 `threshold_mb`,以 MB 展示触发时快照,说明包含加油包。套餐接口和套餐选项中的 `real_data_mb` 保留。 - M2:**BREAKING**:店铺业务员批量换绑说明及前端内置模板(如有)改用固定表头 `店铺编码,操作类型,平台登录账号,备注`,注明换绑时只填写平台用户登录手机号、清空时该列留空;不兼容旧表头,保留逐行失败原因。 - M23:设备导入文案将 IMEI 列从可选改为必填,不改变现有模板列名、列序、请求结构,也不新增 IMEI 长度、格式或重复校验;失败详情继续展示后端行号和原因。 @@ -51,7 +52,7 @@ ## Impact -- Affected specs: `order-management`、`admin-authentication`、`employee-collection`、`refund-management`、`agent-distribution-registration`、`agent-recharge`、`commission-management`、`exchange-management`、`package-traffic-alert`、`shop-management`、`device-batch-import`。 +- Affected specs: `order-management`、`admin-authentication`、`employee-collection`、`refund-management`、`agent-distribution-registration`、`agent-recharge`、`commission-management`、`exchange-management`、`phone-asset-association-management`、`package-traffic-alert`、`shop-management`、`device-batch-import`。 - Affected code: - `src/types/api/{order,account,employeeCollection,refund,agentDistributionRegistration,agentRecharge,commission,packageTrafficAlert}.ts` 及相关类型导出。 - `src/api/modules/{order,account,exchange}.ts`;其余审批相关服务仅按既有接口复用,不扩展 C 端请求。 diff --git a/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md b/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md index e34b0ec..1d21baf 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md +++ b/openspec/changes/update-october-batch-one-admin-integration/specs/exchange-management/spec.md @@ -42,3 +42,20 @@ - **THEN** 页面 MUST 继续按原业务文案及统一错误分类反馈,不重复弹错 - **AND** MUST NOT 新增依据扫码历史或退款状态的前置拒绝逻辑 - **AND** 当前换货迁移字段、角色限制和接口成功行为 MUST 保持不变 + +### Requirement: 换货冲突跳转携带新资产与手机号 + +收到 1202 后,用户点击「前往手机号资产关联」时,后台 SHALL 通过 `asset_identifier` 和 `phone` 路由查询参数传递本次提交的新资产标识及关联手机号。创建换货 MUST 使用提交时选中的新卡 ICCID 或设备虚拟号;确认完成 MUST 使用换货单的新资产标识,不得带入旧资产。手机号 MUST 优先使用错误响应 `data.phone`,缺失时可取新资产 `associated_phones` 的首个有效值。 + +#### Scenario: 创建换货选择新资产后被拒 + +- **WHEN** 创建请求因新资产已有手机号关联返回 1202,用户选择跳转 +- **THEN** 路由 MUST 携带选中新资产的 `asset_identifier` 与对应 `phone` +- **AND** 新旧资产同时存在手机号关联时 MUST NOT 使用旧资产标识或旧手机号 +- **AND** 长 ICCID MUST 保持字符串,不转为数值 + +#### Scenario: 确认完成失败后跳转 + +- **WHEN** 确认完成返回 1202,用户选择跳转 +- **THEN** 路由 MUST 携带该换货单的 `new_asset_identifier` 和错误响应手机号 +- **AND** 参数不完整时 MUST 不猜测缺失值或自动执行解绑 diff --git a/openspec/changes/update-october-batch-one-admin-integration/specs/phone-asset-association-management/spec.md b/openspec/changes/update-october-batch-one-admin-integration/specs/phone-asset-association-management/spec.md new file mode 100644 index 0000000..0f38713 --- /dev/null +++ b/openspec/changes/update-october-batch-one-admin-integration/specs/phone-asset-association-management/spec.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: 按换货新资产与手机号预填查询并勾选 + +手机号资产关联页面 SHALL 读取路由 `asset_identifier` 和 `phone`,预填筛选并从 `page=1,page_size=20` 请求列表。用户具备原批量解绑权限时,页面 MUST 默认勾选同时匹配资产标识、手机号且 `status=1` 的当前页记录,旧资产、其他手机号和已失效记录不得勾选。 + +#### Scenario: 从换货冲突入口进入 + +- **WHEN** 路由携带新资产和手机号参数 +- **THEN** 页面 MUST 使用两项参数调用 `GET /api/admin/phone-asset-associations` +- **AND** 数据渲染后 MUST 实际勾选匹配有效记录,使已有批量解绑按钮可用 +- **AND** MUST 保留解除原因必填与二次确认,不自动发送解绑请求 + +#### Scenario: 缓存页面再次接收另一资产 + +- **WHEN** 关联页面已存在缓存或同一路径查询参数改变 +- **THEN** 页面 MUST 重置分页、筛选和旧勾选并重新查询新的两项条件 +- **AND** 过期请求 MUST NOT 覆盖最新数据或选中新目标以外的行 + +#### Scenario: 参数缺失、手动筛选或解绑后刷新 + +- **WHEN** 两项参数不完整、用户修改筛选/翻页、手工取消选择或完成批量解绑 +- **THEN** 页面 MUST NOT 将无关记录全选或恢复旧勾选 +- **AND** 无批量解绑权限时 MUST NOT 显示可执行的批量解绑入口 + +### Requirement: 批量解绑逐项结果使用中文 + +批量解绑结果表 SHALL 将 `success=true` 展示为「解绑成功」,`success=false` 展示为「解绑失败」,保留中文汇总、解除关系数及后端失败原因,不显示原始布尔值。 + +#### Scenario: 显示成功或失败结果 + +- **WHEN** 后端返回批量解绑逐项结果 +- **THEN** 结果列 MUST 使用中文成功/失败标签,不显示 `true` 或 `false` +- **AND** 失败原因、统计值及原结果布尔类型 MUST 保持原后端契约 diff --git a/openspec/changes/update-october-batch-one-admin-integration/tasks.md b/openspec/changes/update-october-batch-one-admin-integration/tasks.md index 152f443..d19ae4d 100644 --- a/openspec/changes/update-october-batch-one-admin-integration/tasks.md +++ b/openspec/changes/update-october-batch-one-admin-integration/tasks.md @@ -59,3 +59,10 @@ - [ ] 7.11 完成真实测试环境联调及用户验收后归档,不以模拟页面替代后端验收。 7.2、7.3、7.5、7.7 的本地逻辑与部分模拟页面已覆盖;尚未验证真实支付、冻结/预占金额、解除后重试、导入结果与导出文件。复现方式及证据见 [实施记录](./implementation.md)。 + +## 8. 换货冲突关联管理快捷处理(用户追加) + +- [x] 8.1 两个换货入口携带当前新资产标识和关联手机号跳转,提交时冻结目标,禁止旧资产回退。 +- [x] 8.2 关联页预填两项筛选,默认 1 页/20 条,仅勾选匹配有效记录;缓存、参数变化、过期响应和手工筛选清除旧选择。 +- [x] 8.3 复用原批量解绑权限、原因与二次确认,结果布尔值改中文成功/失败。 +- [x] 8.4 66 项回归测试通过,本地完整跳转、实际勾选、手工取消及模拟批量结果验证通过;未操作真实资产。 diff --git a/scripts/tests/octoberBatchOne.test.mjs b/scripts/tests/octoberBatchOne.test.mjs index 8277794..65dea25 100644 --- a/scripts/tests/octoberBatchOne.test.mjs +++ b/scripts/tests/octoberBatchOne.test.mjs @@ -226,6 +226,91 @@ test('exchange phone conflicts support HTTP error and business response without }) }) +test('exchange association handoff carries only the new asset and its bound phone as strings', async () => { + const { getExchangePhoneAssociationQuery } = await loadModule( + 'src/utils/business/exchangeError.ts' + ) + const newAsset = { asset_identifier: '89860624590014594318', associated_phones: ['19232713115'] } + assert.deepEqual( + getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '19232713115' } }, newAsset), + { + asset_identifier: '89860624590014594318', + phone: '19232713115' + } + ) + assert.deepEqual( + getExchangePhoneAssociationQuery({ response: { data: { code: 1202 } } }, newAsset), + { + asset_identifier: '89860624590014594318', + phone: '19232713115' + } + ) + assert.equal(getExchangePhoneAssociationQuery({ code: 1206 }, newAsset), undefined) + assert.equal( + getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '19232713115' } }, {}), + undefined + ) + assert.equal( + getExchangePhoneAssociationQuery( + { code: 1202 }, + { asset_identifier: '89860624590014594318', associated_phones: [] } + ), + undefined + ) + assert.deepEqual( + getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '13900000000' } }, newAsset), + { + asset_identifier: '89860624590014594318', + phone: '13900000000' + } + ) +}) + +test('association preselection excludes old assets, different phones and invalid historical rows', async () => { + const { + getPhoneAssetAssociationTarget, + getMatchingPhoneAssetAssociations, + getPhoneAssetUnbindResultName + } = await loadModule('src/utils/business/phoneAssetAssociation.ts') + const target = getPhoneAssetAssociationTarget({ + asset_identifier: ' 89860624590014594318 ', + phone: '19232713115' + }) + const rows = [ + { id: 1, asset_identifier: target.asset_identifier, phone: target.phone, status: 1 }, + { id: 2, asset_identifier: '89860625770070732445', phone: target.phone, status: 1 }, + { id: 3, asset_identifier: target.asset_identifier, phone: '13900000000', status: 1 }, + { id: 4, asset_identifier: target.asset_identifier, phone: target.phone, status: 0 } + ] + assert.deepEqual( + getMatchingPhoneAssetAssociations(rows, target).map((row) => row.id), + [1] + ) + assert.deepEqual(getMatchingPhoneAssetAssociations(rows, { ...target, phone: '' }), []) + assert.deepEqual( + getPhoneAssetAssociationTarget({ asset_identifier: ['a', 'b'], phone: ['19232713115'] }), + { asset_identifier: '', phone: '' } + ) + assert.equal(getPhoneAssetUnbindResultName(true), '解绑成功') + assert.equal(getPhoneAssetUnbindResultName(false), '解绑失败') +}) + +test('association page watches handoff changes, clears stale selections and retains manual unbind confirmation', async () => { + const page = await readFile( + 'src/views/asset-management/phone-asset-association/index.vue', + 'utf8' + ) + assert.match(page, /getPhoneAssetAssociationTarget\(route.query\)/) + assert.match(page, /toggleRowSelection\(row, true\)/) + assert.match(page, /requestId !== listRequestId/) + assert.match(page, /getPhoneAssetUnbindResultName\(row.success\)/) + assert.doesNotMatch(page, /prop="success"/) + assert.match(page, /batchForm.confirmed = false/) + assert.match(page, /batchFormRef.value\?\.validate/) + const table = await readFile('src/components/core/tables/ArtTable.vue', 'utf8') + assert.match(table, /clearSelection:.*clearSelection\(\)/) +}) + test('password validation errors accept backend message as well as msg', async () => { const { normalizeApiError } = await loadModule('src/utils/business/apiError.ts') for (const field of ['msg', 'message']) { diff --git a/scripts/tests/octoberBrowserFixtures.mjs b/scripts/tests/octoberBrowserFixtures.mjs index b52e9c5..813d05b 100644 --- a/scripts/tests/octoberBrowserFixtures.mjs +++ b/scripts/tests/octoberBrowserFixtures.mjs @@ -101,11 +101,55 @@ const exchange = { updated_at: time } +const associations = [ + { + id: 101, + asset_id: 1001, + asset_type: 'iot_card', + asset_identifier: exchange.new_asset_identifier, + phone: '13800000000', + status: 1, + status_name: '有效', + established_at: time + }, + { + id: 102, + asset_id: 1001, + asset_type: 'iot_card', + asset_identifier: exchange.new_asset_identifier, + phone: '13800000000', + status: 0, + status_name: '已失效', + established_at: time + }, + { + id: 103, + asset_id: 1002, + asset_type: 'iot_card', + asset_identifier: exchange.old_asset_identifier, + phone: '13800000000', + status: 1, + status_name: '有效', + established_at: time + }, + { + id: 104, + asset_id: 1002, + asset_type: 'iot_card', + asset_identifier: exchange.old_asset_identifier, + phone: '13900000000', + status: 1, + status_name: '有效', + established_at: time + } +] + export const mockApiPlugin = { name: 'october-local-mock-api', configureServer(server) { server.middlewares.use((req, res, next) => { - const path = new URL(req.url, 'http://127.0.0.1').pathname + const url = new URL(req.url, 'http://127.0.0.1') + const path = url.pathname if (!path.startsWith('/api/')) return next() if (path === '/api/auth/login' || path === '/api/auth/me') { const user = { @@ -134,6 +178,37 @@ export const mockApiPlugin = { return } if (req.method !== 'GET') { + if (path === '/api/admin/phone-asset-associations/batch-unbind') { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + req.on('end', () => { + const request = JSON.parse(body) + const items = request.assets.map((asset) => { + const matches = associations.filter( + (row) => + row.asset_id === asset.asset_id && + row.asset_type === asset.asset_type && + row.status === 1 + ) + matches.forEach((row) => { + row.status = 0 + row.status_name = '已失效' + }) + return { ...asset, success: true, unbound_count: matches.length, reason: '' } + }) + res.setHeader('Content-Type', 'application/json; charset=utf-8') + res.end( + JSON.stringify({ + code: 0, + msg: 'mock', + data: { success_count: items.length, fail_count: 0, items } + }) + ) + }) + return + } res.setHeader('Content-Type', 'application/json; charset=utf-8') res.end( JSON.stringify({ @@ -153,6 +228,15 @@ export const mockApiPlugin = { page_size: 20 }) let data = page([]) + if (path === '/api/admin/phone-asset-associations') + data = page( + associations.filter( + (row) => + (!url.searchParams.get('asset_identifier') || + row.asset_identifier === url.searchParams.get('asset_identifier')) && + (!url.searchParams.get('phone') || row.phone === url.searchParams.get('phone')) + ) + ) if (path === '/api/admin/orders/1') data = order if (path === '/api/admin/exchanges') data = page([exchange]) if (path === '/api/admin/exchanges/1') data = exchange diff --git a/src/components/core/tables/ArtTable.vue b/src/components/core/tables/ArtTable.vue index 44b18b5..ecc1262 100644 --- a/src/components/core/tables/ArtTable.vue +++ b/src/components/core/tables/ArtTable.vue @@ -234,6 +234,9 @@ // 提供给父组件的方法 defineExpose({ + clearSelection: () => tableRef.value?.clearSelection(), + toggleRowSelection: (row: any, selected?: boolean) => + tableRef.value?.toggleRowSelection(row, selected), // 展开所有行 expandAll: () => { const elTable = tableRef.value diff --git a/src/utils/business/exchangeError.ts b/src/utils/business/exchangeError.ts index 6bb531d..26e137f 100644 --- a/src/utils/business/exchangeError.ts +++ b/src/utils/business/exchangeError.ts @@ -22,3 +22,26 @@ export const getExchangeBusinessErrorMessage = (value: unknown): string | undefi const failure = getBusinessFailure(value) return failure?.msg || failure?.message } + +export interface ExchangeNewAssetContext { + asset_identifier?: string + associated_phones?: string[] | null +} + +// 新资产上下文由提交时选中的资产提供,绝不回退到旧资产。 +export const getExchangePhoneAssociationQuery = ( + failure: unknown, + newAsset: ExchangeNewAssetContext +): { asset_identifier: string; phone: string } | undefined => { + const response = getBusinessFailure(failure) + if (Number(response?.code) !== 1202) return undefined + const asset_identifier = newAsset.asset_identifier?.trim() || '' + const responsePhone = typeof response?.data?.phone === 'string' ? response.data.phone.trim() : '' + const phone = + responsePhone || + newAsset.associated_phones + ?.find((value) => typeof value === 'string' && value.trim()) + ?.trim() || + '' + return asset_identifier && phone ? { asset_identifier, phone } : undefined +} diff --git a/src/utils/business/phoneAssetAssociation.ts b/src/utils/business/phoneAssetAssociation.ts new file mode 100644 index 0000000..0442c03 --- /dev/null +++ b/src/utils/business/phoneAssetAssociation.ts @@ -0,0 +1,30 @@ +import type { PhoneAssetAssociation } from '@/types/api/phoneAsset' + +export interface PhoneAssetAssociationTarget { + asset_identifier: string + phone: string +} + +// 只接受单个字符串,拒绝重复 query 参数导致的歧义,不把长 ICCID 转成数值。 +export const getPhoneAssetAssociationTarget = ( + query: Record +): PhoneAssetAssociationTarget => ({ + asset_identifier: typeof query.asset_identifier === 'string' ? query.asset_identifier.trim() : '', + phone: typeof query.phone === 'string' ? query.phone.trim() : '' +}) + +export const getMatchingPhoneAssetAssociations = ( + rows: PhoneAssetAssociation[], + target: PhoneAssetAssociationTarget +): PhoneAssetAssociation[] => { + if (!target.asset_identifier || !target.phone) return [] + return rows.filter( + (row) => + row.status === 1 && + row.asset_identifier === target.asset_identifier && + row.phone === target.phone + ) +} + +export const getPhoneAssetUnbindResultName = (success: boolean): string => + success ? '解绑成功' : '解绑失败' diff --git a/src/views/asset-management/exchange-management/index.vue b/src/views/asset-management/exchange-management/index.vue index 04a631c..2a834ef 100644 --- a/src/views/asset-management/exchange-management/index.vue +++ b/src/views/asset-management/exchange-management/index.vue @@ -446,7 +446,9 @@ import { normalizeApiError } from '@/utils/business/apiError' import { getExchangePhoneConflictMessage, - getExchangeBusinessErrorMessage + getExchangeBusinessErrorMessage, + getExchangePhoneAssociationQuery, + type ExchangeNewAssetContext } from '@/utils/business/exchangeError' defineOptions({ name: 'ExchangeManagement' }) @@ -455,7 +457,11 @@ const userStore = useUserStore() const router = useRouter() - const handleExchangeFailure = async (failure: unknown, fallback: string) => { + const handleExchangeFailure = async ( + failure: unknown, + fallback: string, + newAsset: ExchangeNewAssetContext = {} + ) => { const conflictMessage = getExchangePhoneConflictMessage(failure) if (!conflictMessage) { const normalized = normalizeApiError(failure) @@ -467,6 +473,7 @@ return } const canManageAssociations = hasAuth(AUGUST_PERMISSIONS.phoneAssetAssociation.list) + const associationQuery = getExchangePhoneAssociationQuery(failure, newAsset) try { await ElMessageBox.confirm( canManageAssociations @@ -481,7 +488,8 @@ dangerouslyUseHTMLString: false } ) - if (canManageAssociations) await router.push(RoutesAlias.PhoneAssetAssociation) + if (canManageAssociations) + await router.push({ path: RoutesAlias.PhoneAssetAssociation, query: associationQuery }) } catch { // 用户关闭提示,不执行解绑或改写换货状态。 } @@ -735,6 +743,7 @@ type ShopSelectableOption = { shop_id?: number | null shop_name?: string | null + associated_phones?: string[] } const getShopDisplayName = (option?: ShopSelectableOption | null) => { @@ -1552,6 +1561,11 @@ createFormRef.value?.validate(async (valid) => { if (!valid || createLoading.value) return + // 提交时冻结新资产上下文;请求或提示期间改动表单也不能跳到其他资产。 + const newAsset: ExchangeNewAssetContext = { + asset_identifier: createForm.new_identifier, + associated_phones: [...(createSelectedNewAsset.value?.associated_phones || [])] + } createLoading.value = true try { const exchangeReason = @@ -1583,9 +1597,9 @@ return } - await handleExchangeFailure(res, '换货单创建失败') + await handleExchangeFailure(res, '换货单创建失败', newAsset) } catch (error) { - await handleExchangeFailure(error, '换货单创建失败') + await handleExchangeFailure(error, '换货单创建失败', newAsset) } finally { createLoading.value = false } @@ -1810,6 +1824,9 @@ const handleCompleteExchange = async (row: ExchangeResponse) => { if (completeLoadingId.value === row.id) return + const newAsset: ExchangeNewAssetContext = { + asset_identifier: row.new_asset_identifier || undefined + } completeLoadingId.value = row.id try { @@ -1829,11 +1846,11 @@ // 刷新失败也不能吞掉原操作的 1202 业务响应。 await Promise.allSettled([refreshExchangeDetail(row.id), loadExchangeList()]) if (res.code === 0) ElMessage.success('操作成功') - else await handleExchangeFailure(res, '确认完成失败') + else await handleExchangeFailure(res, '确认完成失败', newAsset) } catch (error) { // 仅重新读取原单状态,不乐观标记完成,错误对象可能包含完整手机号,不输出日志。 await Promise.allSettled([refreshExchangeDetail(row.id), loadExchangeList()]) - await handleExchangeFailure(error, '确认完成失败') + await handleExchangeFailure(error, '确认完成失败', newAsset) } finally { completeLoadingId.value = null } diff --git a/src/views/asset-management/phone-asset-association/index.vue b/src/views/asset-management/phone-asset-association/index.vue index 89fc619..fabe1ef 100644 --- a/src/views/asset-management/phone-asset-association/index.vue +++ b/src/views/asset-management/phone-asset-association/index.vue @@ -2,7 +2,8 @@
失败 {{ batchResult?.fail_count ?? 0 }} 项
- + - + @@ -207,8 +220,19 @@