From f0a2b84e5326c729e93715286ddd0abe946e1b2a Mon Sep 17 00:00:00 2001 From: luo Date: Mon, 27 Jul 2026 16:30:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A7=E5=93=81=E8=BF=AD=E4=BB=A37?= =?UTF-8?q?=E6=9C=88=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add-asset-realname-status-filter/tasks.md | 24 +- .../tasks.md | 2 +- .../tasks.md | 22 +- .../design.md | 42 ++ .../proposal.md | 23 + .../spec.md | 193 ++++++++ .../tasks.md | 61 +++ .../tasks.md | 26 +- public/templates/bulk-purchase-template.csv | 1 + .../device-batch-allocation-template.csv | 1 + src/api/modules/account.ts | 6 +- src/api/modules/bulkPurchase.ts | 30 +- src/api/modules/card.ts | 18 +- src/api/modules/device.ts | 32 +- src/api/modules/exchange.ts | 20 +- src/api/modules/exportTask.ts | 3 +- src/api/modules/index.ts | 1 + src/api/modules/packageManage.ts | 4 +- src/api/modules/storage.ts | 7 +- src/api/modules/wecom.ts | 78 ++++ src/components/business/VoucherUpload.vue | 36 +- src/composables/useAsyncTaskPolling.ts | 4 +- src/config/constants/exportTask.ts | 50 +++ src/config/constants/index.ts | 1 + src/config/constants/julyIteration.ts | 32 ++ src/router/routes/asyncRoutes.ts | 97 +++- src/router/routesAlias.ts | 7 +- src/types/api/agentRecharge.ts | 11 +- src/types/api/asset.ts | 16 +- src/types/api/bulkPurchase.ts | 65 ++- src/types/api/card.ts | 10 + src/types/api/device.ts | 31 +- src/types/api/exportTask.ts | 14 +- src/types/api/index.ts | 3 + src/types/api/order.ts | 17 +- src/types/api/packageManagement.ts | 7 +- src/types/api/refund.ts | 5 +- src/types/api/shop.ts | 4 + src/types/api/systemConfig.ts | 19 + src/types/api/wecom.ts | 95 ++++ src/types/components.d.ts | 1 + src/utils/business/apiError.ts | 31 ++ src/utils/business/approvalSummary.ts | 29 +- src/utils/http/index.ts | 6 +- .../components/BasicInfoCard.vue | 37 +- .../dialogs/PackageRechargeDialog.vue | 8 +- .../components/dialogs/SpeedLimitDialog.vue | 54 +-- .../composables/useAssetOperations.ts | 32 -- .../asset-information/index.vue | 34 +- .../asset-management/device-list/index.vue | 3 +- .../exchange-management/detail.vue | 5 + .../exchange-management/index.vue | 29 +- .../expiring-assets/index.vue | 18 +- .../iot-card-management/index.vue | 3 +- .../task-management/device-task/index.vue | 203 +++++++-- .../task-management/task-detail/index.vue | 9 + .../agent-fund-overview/index.vue | 15 +- src/views/finance/agent-recharge/detail.vue | 6 +- src/views/finance/agent-recharge/index.vue | 6 +- .../order-management/bulk-purchase/index.vue | 107 ++--- .../order-management/order-list/detail.vue | 32 +- .../order-management/order-list/index.vue | 48 +- .../package-list/detail.vue | 24 +- .../package-management/package-list/index.vue | 153 +++++-- .../package-series/index.vue | 10 +- .../series-grants/detail.vue | 41 +- .../series-grants/index.vue | 45 +- .../series-grants/packages.vue | 26 +- src/views/settings/system-configs/index.vue | 34 +- .../settings/wecom/applications/detail.vue | 253 +++++++++++ .../settings/wecom/applications/index.vue | 171 +++++++ src/views/settings/wecom/members/index.vue | 424 ++++++++++++++++++ src/views/settings/wecom/scenes/index.vue | 375 ++++++++++++++++ src/views/shop-management/list/index.vue | 52 ++- src/views/system/role/index.vue | 18 +- 75 files changed, 2926 insertions(+), 534 deletions(-) create mode 100644 openspec/changes/update-july-iteration-frontend-integration/design.md create mode 100644 openspec/changes/update-july-iteration-frontend-integration/proposal.md create mode 100644 openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md create mode 100644 openspec/changes/update-july-iteration-frontend-integration/tasks.md create mode 100644 public/templates/bulk-purchase-template.csv create mode 100644 public/templates/device-batch-allocation-template.csv create mode 100644 src/api/modules/wecom.ts create mode 100644 src/config/constants/julyIteration.ts create mode 100644 src/types/api/wecom.ts create mode 100644 src/utils/business/apiError.ts rename src/views/{operations => asset-management}/expiring-assets/index.vue (94%) create mode 100644 src/views/settings/wecom/applications/detail.vue create mode 100644 src/views/settings/wecom/applications/index.vue create mode 100644 src/views/settings/wecom/members/index.vue create mode 100644 src/views/settings/wecom/scenes/index.vue diff --git a/openspec/changes/add-asset-realname-status-filter/tasks.md b/openspec/changes/add-asset-realname-status-filter/tasks.md index d77ccf1..b713eb0 100644 --- a/openspec/changes/add-asset-realname-status-filter/tasks.md +++ b/openspec/changes/add-asset-realname-status-filter/tasks.md @@ -1,26 +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/standalone` 并传递实名状态筛选参数。 +- [x] 1.1 扩展卡列表查询参数和列表项类型,支持可选 `real_name_status` 以及必需的 `real_name_status_name`。 +- [x] 1.2 扩展设备列表查询参数和列表项类型,支持可选 `real_name_status` 以及必需的 `real_name_status_name`。 +- [x] 1.3 将卡列表查询接入 `GET /api/admin/iot-cards/standalone` 并传递实名状态筛选参数。 ## 2. Card List -- [ ] 2.1 在卡列表筛选区新增“实名状态”的全部、已实名、未实名选项。 -- [ ] 2.2 将选中的实名状态传递给卡列表查询,并在搜索、刷新、分页、导出中保留该条件。 -- [ ] 2.3 在卡列表表格新增“实名状态”列,展示接口返回的 `real_name_status_name`。 -- [ ] 2.4 重置卡列表搜索时清空实名状态筛选。 +- [x] 2.1 在卡列表筛选区新增“实名状态”的全部、已实名、未实名选项。 +- [x] 2.2 将选中的实名状态传递给卡列表查询,并在搜索、刷新、分页、导出中保留该条件。 +- [x] 2.3 在卡列表表格新增“实名状态”列,展示接口返回的 `real_name_status_name`。 +- [x] 2.4 重置卡列表搜索时清空实名状态筛选。 ## 3. Device List -- [ ] 3.1 在设备列表筛选区新增“实名状态”的全部、已实名、未实名选项。 -- [ ] 3.2 将选中的实名状态传递给 `GET /api/admin/devices`,并在搜索、刷新、分页、导出中保留该条件。 -- [ ] 3.3 在设备列表表格新增“实名状态”列,直接展示接口返回的 `real_name_status_name`,不遍历绑定卡计算状态。 -- [ ] 3.4 重置设备列表搜索时清空实名状态筛选。 +- [x] 3.1 在设备列表筛选区新增“实名状态”的全部、已实名、未实名选项。 +- [x] 3.2 将选中的实名状态传递给 `GET /api/admin/devices`,并在搜索、刷新、分页、导出中保留该条件。 +- [x] 3.3 在设备列表表格新增“实名状态”列,直接展示接口返回的 `real_name_status_name`,不遍历绑定卡计算状态。 +- [x] 3.4 重置设备列表搜索时清空实名状态筛选。 ## 4. Verification - [ ] 4.1 验证卡列表“全部”“已实名”“未实名”分别不传、传 `1`、传 `0`,且状态列显示接口名称。 - [ ] 4.2 验证设备列表“全部”“已实名”“未实名”分别不传、传 `1`、传 `0`,且状态列直接显示设备接口名称。 - [ ] 4.3 验证两个列表在分页切换、刷新和导出时保留实名状态筛选,重置后清空该条件。 -- [ ] 4.4 运行相关类型检查、lint 或构建验证。 +- [x] 4.4 运行相关类型检查、lint 或构建验证。 diff --git a/openspec/changes/update-exchange-refund-interception-feedback/tasks.md b/openspec/changes/update-exchange-refund-interception-feedback/tasks.md index fed8e1f..de521b4 100644 --- a/openspec/changes/update-exchange-refund-interception-feedback/tasks.md +++ b/openspec/changes/update-exchange-refund-interception-feedback/tasks.md @@ -18,5 +18,5 @@ - [ ] 4.1 验证活跃退款资产创建换货时显示“该资产存在退款申请”或后端等价业务原因,且不创建换货单。 - [ ] 4.2 验证已拒绝、撤销或处理完成退款的资产按后端结果可继续创建换货。 -- [ ] 4.3 验证失败后保留表单并可更换资产重新提交。 +- [x] 4.3 验证失败后保留表单并可更换资产重新提交。 - [x] 4.4 运行相关前端校验,并执行 `openspec validate update-exchange-refund-interception-feedback --strict`。 diff --git a/openspec/changes/update-exchange-separate-asset-search-display/tasks.md b/openspec/changes/update-exchange-separate-asset-search-display/tasks.md index 596cf26..026874d 100644 --- a/openspec/changes/update-exchange-separate-asset-search-display/tasks.md +++ b/openspec/changes/update-exchange-separate-asset-search-display/tasks.md @@ -1,21 +1,21 @@ ## 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`。 +- [x] 1.1 将换货列表查询类型中的通用 `identifier` 替换为可选的 `old_asset_keyword` 与 `new_asset_keyword`。 +- [x] 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 原始值,不在前端转换后再传递给后端。 +- [x] 2.1 将换货管理列表筛选区的“资产标识符”替换为“旧资产”和“新资产”输入框。 +- [x] 2.2 查询、刷新和分页时保留并传递非空的 `old_asset_keyword`、`new_asset_keyword`,不再发送 `identifier`。 +- [x] 2.3 重置搜索条件时清空新旧资产关键词,后续请求不发送这两个参数。 +- [x] 2.4 保留用户输入的 ICCID、接入号、虚拟号、IMEI 或 SN 原始值,不在前端转换后再传递给后端。 ## 3. Exchange List Display -- [ ] 3.1 增加旧资产类型、旧资产标识、新资产类型、新资产标识四个独立列表列。 -- [ ] 3.2 根据后端资产类型显示“物联网卡”或“设备”,并原样显示对应后端资产标识。 -- [ ] 3.3 状态列优先展示后端 `status_name`,缺失时显示稳定占位内容。 -- [ ] 3.4 新资产字段在未分配时显示稳定占位内容,不与旧资产字段混用。 +- [x] 3.1 增加旧资产类型、旧资产标识、新资产类型、新资产标识四个独立列表列。 +- [x] 3.2 根据后端资产类型显示“物联网卡”或“设备”,并原样显示对应后端资产标识。 +- [x] 3.3 状态列优先展示后端 `status_name`,缺失时显示稳定占位内容。 +- [x] 3.4 新资产字段在未分配时显示稳定占位内容,不与旧资产字段混用。 ## 4. Verification @@ -23,4 +23,4 @@ - [ ] 4.2 验证两个资产关键词均为空时不传筛选参数,重置后保持相同行为。 - [ ] 4.3 验证旧资产和新资产类型、标识符分别显示,IoT 卡显示 ICCID、设备显示设备号。 - [ ] 4.4 验证卡的 ICCID、接入号或虚拟号关键词均由后端按输入所在的旧/新资产条件命中,前端不转换输入值。 -- [ ] 4.5 运行相关类型检查、lint 或构建验证。 +- [x] 4.5 运行相关类型检查、lint 或构建验证。 diff --git a/openspec/changes/update-july-iteration-frontend-integration/design.md b/openspec/changes/update-july-iteration-frontend-integration/design.md new file mode 100644 index 0000000..ef7d3bc --- /dev/null +++ b/openspec/changes/update-july-iteration-frontend-integration/design.md @@ -0,0 +1,42 @@ +## Context + +七月迭代后端实现已形成一份统一的前端联调说明,但需求横跨多个后台业务域,并同时涉及 H5/C 端接口。若不先固定边界,前端容易继续依赖旧字段、重复计算服务端事实,或误把 C 端工作纳入后台改造。 + +## Goals / Non-Goals + +- Goals: 固定后台页面的调用顺序、字段来源、权限边界、金额单位、异步任务和错误处理规则。 +- Goals: 明确 H5/C 端依赖并标注为外部范围,不在本提案任务中实施。 +- Non-Goals: 不新增后端接口,不修改数据库,不实现企微/Gateway/Redis/Asynq/对象存储服务端逻辑。 +- Non-Goals: 不修改 H5 或 C 端页面、组件和调用代码。 + +## Decisions + +- Decision: 后端返回值是业务事实来源。前端不得自行推导实名状态、支付方式、预计到期、可用金额、欠款、审批状态或异步任务状态。 +- Decision: 金额统一以分传输和计算,展示层才转换为元;信用和资金更新必须携带服务端返回的 `version`。 +- Decision: H5/C 端接口在规格中单独列为外部依赖,后台只负责在需要时保持接口契约一致。 +- Decision: 企微审批继续由原退款/线下充值业务入口触发,后台只读展示 `approval_provider`、`approval_status` 和 `approval_status_name`,不恢复旧人工审批按钮。 +- Decision: 设备批量分配复用设备导入任务页面和任务外壳,通过 `operation_type` 区分分配代理与设置套餐系列。 +- Decision: 系列套餐授权使用现有 `packages[]` 数组完成多选,授权详情中的 `package_id` 用于已授权标记,套餐列表中的公司成本价与授权成本价分开使用。 +- Decision: `docs/所需接口文档` 是本变更的接口契约索引;页面 API 类型、请求参数、枚举和响应字段必须以对应模块文档为准,不从七月说明中自行扩展路径。 +- Decision: 文件类业务统一采用 `POST /api/admin/storage/upload-url` 获取预签名 URL,直传成功后仅把 `file_key` 传给批量订购或设备分配接口。 +- Decision: 批量订购、设备分配和导出均保存服务端返回的任务 ID;页面恢复时查询原任务,终态后停止轮询,不能因刷新或超时重复创建。 + +## Risks / Trade-offs + +- 接口 DTO 若未按说明返回,页面无法可靠展示审批人、历史资产标识或新旧字段;通过联调清单阻断实现前确认。 +- 设备批量分配接口文档已明确任务列表和详情仅平台用户可操作,前端不得向代理账号展示任务入口或通过前端绕过权限。 +- H5/C 端不在本次实现范围,后台只能依赖后端提供的最终字段,跨端验收需要单独安排。 +- 部分接口文档只给出字段模型,缺少业务错误码、权限编码和分页默认值;这些列入待确认清单。 + +## Migration Plan + +1. 先确认接口待补充项和后台权限编码。 +2. 按任务清单分模块适配后台页面和 API 类型。 +3. 完成后台接口 Mock/联调、权限组合和错误场景验收。 +4. H5/C 端另行创建或关联变更,不在本变更中合并实现。 + +## Open Questions + +- 退款和代理充值详情是否需要直接返回企微审批节点人员列表? +- 各新增后台页面和按钮的最终权限编码是什么? +- 批量任务、导出任务、企微审批和 Gateway 超时的稳定错误码及重试语义是什么? diff --git a/openspec/changes/update-july-iteration-frontend-integration/proposal.md b/openspec/changes/update-july-iteration-frontend-integration/proposal.md new file mode 100644 index 0000000..460086e --- /dev/null +++ b/openspec/changes/update-july-iteration-frontend-integration/proposal.md @@ -0,0 +1,23 @@ +# Change: 对接七月迭代后台实现与接口契约 + +## Why + +最新的七月迭代实现说明明确了后端已完成的接口、字段语义、前端调用顺序和联调边界。当前后台页面仍需要按这些最新契约完成筛选、展示、权限、审批、批量任务、导出和代理资金能力的统一对接,避免继续使用旧字段或前端自行推导业务规则。 + +## What Changes + +- 按 `00-七月迭代实现与接口对接说明.md` 更新后台管理端的接口调用和页面展示契约。 +- 以 `docs/所需接口文档/01-店铺.md` 至 `21-设备批量分配.md` 为接口参数、响应字段、枚举、权限和错误语义的具体来源。 +- 对资产、店铺、订单、换货、套餐、资金、系列授权、审批、通知、批量任务、导出和 Gateway 相关页面补齐前端适配要求。 +- 统一使用后端返回的实名策略、支付方式、预计到期、审批状态、金额计算结果和任务状态,不在前端重复推导。 +- 补齐 CSV 上传模板、异步任务恢复、权限按钮、错误处理和并发冲突处理要求。 +- 明确预签名上传先取 `file_key`、再调用批量业务接口的两阶段流程,并固定批量订购、设备分配和导出任务的终态处理规则。 +- **不实施 H5/C 端页面改动**,但在规格和联调清单中标注其依赖的接口及外部工作项。 +- **不实施后端接口、数据库、Worker、企微回调、Gateway 和对象存储改造**;本变更只描述后台前端联调所需的适配边界。 + +## Impact + +- Affected specs: `july-iteration-frontend-integration` +- Affected code: 后台资产、店铺、订单、换货、套餐、代理资金、系列授权、审批配置、通知、批量任务和导出页面 +- External follow-up: H5/C 端认证限制、实名流程、支付按钮、C 端订单续费、C 端通知和 C 端资产展示 +- Approval gate: 接口待确认项完成前不进入大范围实现 diff --git a/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md b/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md new file mode 100644 index 0000000..230db79 --- /dev/null +++ b/openspec/changes/update-july-iteration-frontend-integration/specs/july-iteration-frontend-integration/spec.md @@ -0,0 +1,193 @@ +## ADDED Requirements + +### Requirement: Backend-first field semantics + +The admin frontend SHALL use backend-provided business facts for real-name status, payment methods, estimated expiry, approval status, task status, balances, available amounts, debt amounts, and asset identifiers. The frontend MUST NOT infer or recalculate these values from partial data. + +#### Scenario: Payment methods are displayed from the API + +- **WHEN** the asset or recharge API returns `allowed_payment_methods` +- **THEN** the admin or related integration UI uses that list instead of hard-coded payment rules + +#### Scenario: Financial amounts are displayed from the API + +- **WHEN** a fund or credit response returns calculated balances +- **THEN** the frontend displays the returned values in fen-based data and only converts them for presentation + +### Requirement: Admin asset and shop integration + +The admin frontend SHALL support the confirmed July filters and fields for shops, IoT cards, devices, orders, exchanges, and assets, including contact-phone search, salesperson data, client login restriction, real-name filters, separate old/new exchange searches, asset identifiers, and estimated final expiry. + +#### Scenario: Exchange assets are searched independently + +- **WHEN** a user enters old and new asset search terms +- **THEN** the frontend sends `old_asset_keyword` and `new_asset_keyword` independently and preserves AND filtering semantics + +#### Scenario: A shop login restriction is edited + +- **WHEN** an authorized user updates a shop +- **THEN** the request includes the optional `client_login_disabled` value and the list/detail views display the returned value + +### Requirement: Package allocation and series authorization + +The admin frontend SHALL use the existing package and series authorization APIs for default expiry settings, override expiry settings, package multi-selection, authorization detail lookup, and package add/update/remove operations. + +#### Scenario: A series authorization is edited + +- **WHEN** a user opens an existing authorization +- **THEN** the frontend queries by authorization record ID, marks authorized package IDs, and keeps company cost price separate from authorization cost price + +#### Scenario: Authorized packages are updated + +- **WHEN** a user submits package changes +- **THEN** the frontend sends a 1–100 item `packages[]` array, uses `remove=true` for removals, and loads additional package pages when the series contains more than 100 packages + +### Requirement: Credit and system configuration integration + +The admin frontend SHALL integrate role default credit, shop actual credit, fund summary, and controlled system configuration APIs using fen-based amounts, server-calculated values, optimistic-lock versions, and registered configuration validation metadata. + +#### Scenario: A shop credit limit update conflicts + +- **WHEN** the credit update is rejected because the wallet `version` is stale +- **THEN** the frontend refreshes the latest fund summary and asks the user to retry with the latest version + +#### Scenario: A configuration is read-only + +- **WHEN** a configuration item is returned with `readonly=true` +- **THEN** the frontend disables its editing control and does not submit an update for that item + +### Requirement: WeCom approval status integration + +The admin frontend SHALL provide the confirmed WeCom configuration workflow and SHALL display business approval status as read-only for refund and offline recharge records when the approval provider is WeCom or an approval instance exists. + +#### Scenario: A WeCom business record is awaiting approval + +- **WHEN** a refund or offline recharge record has `approval_provider=wecom` or an `approval_instance_id` +- **THEN** the frontend displays the backend approval status and hides legacy manual approval actions + +#### Scenario: A template mapping is rejected + +- **WHEN** the backend rejects a scene template mapping +- **THEN** the frontend preserves the submitted form values and displays the backend validation message + +### Requirement: Notifications, asynchronous tasks, exports, and Gateway controls + +The admin frontend SHALL integrate low-balance notifications, expiring asset summaries, batch purchase tasks, export tasks, card speed tiers, and device allocation tasks according to the confirmed task, CSV, datasource, and timeout semantics. + +#### Scenario: A batch task is restored + +- **WHEN** a user reloads or re-enters a batch purchase or device allocation page +- **THEN** the frontend queries the existing task detail using the saved task identifier and does not create a duplicate task + +#### Scenario: A speed tier is selected + +- **WHEN** a user operates on an IoT card +- **THEN** the frontend submits a fixed speed-tier code from `-1` through `8`; a device page does not expose a speed-tier control + +### Requirement: H5 and C-end work is explicitly external + +The July integration specification SHALL identify H5/C-end changes as external follow-up work. This change MUST NOT modify H5/C-end pages or their API call implementations. + +#### Scenario: A requirement depends on C-end behavior + +- **WHEN** a requirement involves C-end login restriction, real-name flow, payment methods, asset display, order renewal, or C-end notifications +- **THEN** the item is marked as an external dependency and excluded from this change's implementation tasks + +### Requirement: Permission and error boundaries + +The admin frontend SHALL apply confirmed permission codes to new pages, buttons, and row actions, enforce data isolation in the UI, and present stable user-facing errors for authentication, authorization, validation, timeout, conflict, and unknown-result cases. + +#### Scenario: A user lacks an action permission + +- **WHEN** the current account does not have the configured permission code +- **THEN** the corresponding page action is hidden or disabled and the frontend does not invoke the API + +#### Scenario: An asynchronous operation has an unknown result + +- **WHEN** a task or Gateway operation times out with an unknown result +- **THEN** the frontend preserves the current data, shows a safe status message, and provides a refresh or manual verification path instead of blindly retrying + +### Requirement: Documented API contract alignment + +The admin frontend SHALL implement the July integration against the documented API paths and request/response contracts. The documented interface files under `docs/所需接口文档/` SHALL be treated as the source of truth for parameter names, enum values, nullable fields, pagination, permissions, and HTTP error semantics. + +#### Scenario: A page loads a documented resource + +- **WHEN** a July page queries or mutates a resource +- **THEN** it uses the corresponding documented `/api/admin` endpoint and sends the documented parameter names without translating them to legacy frontend names + +#### Scenario: A response contains a nullable or enumerated field + +- **WHEN** a documented response returns a nullable field, numeric status, string enum, or `*_name` display field +- **THEN** the frontend preserves the nullable state, uses the documented enum values, and prefers the backend display name instead of inventing a local label + +#### Scenario: An API rejects a request + +- **WHEN** a documented endpoint returns `400`, `401`, `403`, or `500` +- **THEN** the frontend applies the shared authentication, authorization, validation, server-error, and retry-safe handling without exposing raw transport details as the user-facing message + +### Requirement: File upload and batch task contract + +The admin frontend SHALL use `POST /api/admin/storage/upload-url` for file-backed July operations, upload to the returned `upload_url` with the requested content type, and pass only the returned `file_key` to the business endpoint. Batch purchase and device allocation CSVs SHALL be UTF-8 single-column files, accept an optional UTF-8 BOM, stay within 10 MB and 1000 data rows, and preserve identifiers as text. + +#### Scenario: A batch purchase is submitted + +- **WHEN** the user selects one package and one payment method and uploads a valid CSV +- **THEN** the frontend calls `POST /api/admin/asset-package-batch-orders` with `file_key`, `package_id`, `payment_method`, and, for offline payment, 1–5 `voucher_keys` + +#### Scenario: A device allocation is submitted + +- **WHEN** a platform user selects an allocation target and uploads a valid device CSV +- **THEN** the frontend calls `POST /api/admin/devices/import/allocations` with `file_key`, `operation_type` set to `assign_shop` or `assign_series`, and the selected `target_id` + +#### Scenario: A batch task is resumed + +- **WHEN** the page is reloaded while a task is non-terminal +- **THEN** the frontend queries the saved task ID, displays the backend `status_name` and progress/result counts, stops polling at the documented terminal state, and never creates a second task + +### Requirement: WeCom configuration API workflow + +The admin frontend SHALL implement the documented WeCom configuration sequence: list or save applications through `/api/admin/wecom/applications`, test the application, synchronize and query visible members, set the default creator, bind system accounts through `/api/admin/accounts/{account_id}/wecom-binding`, and save or query the two supported scene mappings. + +#### Scenario: A WeCom application is configured + +- **WHEN** a super administrator saves valid application credentials +- **THEN** the frontend refreshes the application, allows connection testing, and clearly distinguishes `data.success=true` for access-token acquisition from completed member, template, and callback configuration + +#### Scenario: A default creator is selected + +- **WHEN** the user sets a default creator +- **THEN** the frontend only submits a `userid` selected from the latest synchronized visible-member list and preserves the real local submitter in business records + +#### Scenario: A scene mapping is validated + +- **WHEN** the user saves `refund_approval` or `offline_recharge_approval` +- **THEN** the frontend submits `application_id`, `template_id`, `control_mapping`, and `status`, preserves input on validation failure, and displays the returned verification/status fields after success + +### Requirement: Approval and financial business API contract + +The admin frontend SHALL use the documented refund and agent-recharge APIs for business creation and read-only approval tracking. Amounts and voucher keys SHALL follow the documented units and cardinality, and a record with `approval_provider=wecom` or `approval_instance_id` SHALL not expose legacy manual approval actions. + +#### Scenario: An offline agent recharge is created + +- **WHEN** a platform user submits an offline recharge +- **THEN** the frontend calls `POST /api/admin/agent-recharges` with the target `shop_id`, amount in fen, `payment_method=offline`, and 1–5 voucher keys + +#### Scenario: A refund is awaiting WeCom approval + +- **WHEN** `GET /api/admin/refunds` or `GET /api/admin/refunds/{id}` returns a WeCom approval instance +- **THEN** the frontend displays `approval_status_name`, `submitter_name`, and documented amount fields as read-only and does not show approve/reject actions + +#### Scenario: Approval submission has an unknown result + +- **WHEN** `approval_status=8` or the documented status name indicates an unknown submission result +- **THEN** the frontend does not recreate the business record and offers refresh or manual verification instead + +### Requirement: Documented device allocation permissions + +The admin frontend SHALL enforce the documented permission boundary for device allocation task list and detail APIs. Because those APIs are documented as platform-only, agent and enterprise accounts SHALL not see the allocation task entry or be allowed to invoke the task list/detail endpoints from the UI. + +#### Scenario: A non-platform user opens device allocation + +- **WHEN** an agent or enterprise account loads the device allocation area +- **THEN** the frontend hides the entry and does not call `/api/admin/devices/import/tasks` or `/api/admin/devices/import/tasks/{id}` diff --git a/openspec/changes/update-july-iteration-frontend-integration/tasks.md b/openspec/changes/update-july-iteration-frontend-integration/tasks.md new file mode 100644 index 0000000..ddf1789 --- /dev/null +++ b/openspec/changes/update-july-iteration-frontend-integration/tasks.md @@ -0,0 +1,61 @@ +## 1. Contract and API Types + +- [x] 1.1 根据 `docs/所需接口文档/01-店铺.md` 至 `21-设备批量分配.md` 以及最新说明核对所有纳入本期范围的 API 类型。 +- [x] 1.2 补齐 `purchase_role`、`asset_identifier`、`effective_realname_policy`、`allowed_payment_methods`、预计到期和换货链路字段。 +- [x] 1.3 补齐退款、代理充值和换货的提交人及审批状态字段。 +- [x] 1.4 补齐系统配置、批量任务、导出任务、Gateway 限速和设备分配任务字段。 +- [x] 1.5 为金额、版本号、枚举和 nullable 字段增加明确的前端类型约束。 + +## 2. Asset, Shop, Order, and Exchange Pages + +- [x] 2.1 店铺列表接入联系电话精确查询、业务员筛选和 `client_login_disabled` 展示/编辑。 +- [x] 2.2 资产卡和设备列表接入 `real_name_status` 筛选及状态展示。 +- [x] 2.3 实名策略页面接入单条和最多 500 条批量修改,并处理全成全败结果。 +- [x] 2.4 换货列表拆分旧资产和新资产搜索参数,并展示新旧资产信息。 +- [x] 2.5 资产、订单和 C 端相关后台页面展示正确的订单渠道及资产标识。 +- [x] 2.6 展示预计最终到期时间并按后端临期字段高亮。 +- [x] 2.7 退款中资产创建换货时展示后端业务错误,不调用企微实时查询。 +- [x] 2.8 历史订单或当前资产续费继续调用现有订单创建接口,不新增续费接口。 + +## 3. Package, Fund, and Authorization Pages + +- [x] 3.1 套餐页面传递 `expiry_base` 并展示默认生效条件。 +- [x] 3.2 套餐分配页面传递 `expiry_base_override` 并展示最终生效条件。 +- [x] 3.3 系列授权页面支持 1~100 个套餐多选,创建使用 `packages[]`。 +- [x] 3.4 系列授权编辑使用授权记录 ID 查询详情,区分公司成本价和授权成本价。 +- [x] 3.5 系列授权套餐管理使用 `remove=true` 删除项,并继续分页加载超过 100 个套餐的系列。 +- [x] 3.6 角色默认信用、店铺实际信用和资金概况统一使用分单位字段。 +- [x] 3.7 店铺调额携带资金 `version`,并处理并发冲突刷新和重试。 +- [x] 3.8 系统配置页面接入卡/设备支付方式配置,提交 JSON 数组字符串并保留至少一种支付方式。 + +## 4. WeCom Approval and Business Status + +- [x] 4.1 实现企微应用配置页的应用列表、保存、测试连接和状态展示。 +- [x] 4.2 实现成员同步、成员查询、默认发起人设置和账号绑定入口。 +- [x] 4.3 实现退款和线下代充值场景模板映射配置及后端校验错误保留输入。 +- [x] 4.4 退款、代理充值列表/详情只读展示审批状态,隐藏企微业务的旧人工审批按钮。 +- [x] 4.5 对提交中、审批中、已通过、已拒绝、已撤销、通过后撤销、已删除、提交失败和提交结果未知状态使用后端名称。 + +## 5. Notifications, Async Tasks, Exports, and Gateway + +- [x] 5.1 接入后台低余额通知列表、未读数和已读操作。 +- [x] 5.2 接入临期资产列表、统计摘要、优先级和高亮规则。 +- [x] 5.3 批量订购使用 UTF-8 单列 CSV、上传用途、套餐和支付方式独立选择。 +- [x] 5.4 批量订购任务展示逐行成功/失败结果并支持任务详情恢复。 +- [x] 5.5 导出任务支持本期六类 datasource、筛选条件 `query`、统一任务状态、进度和完成后 24 小时下载链接;保留接口文档中已有的设备/订单场景兼容性。 +- [x] 5.6 卡页面接入 ICCID 固定限速档位 `-1~8`,设备页面不展示限速入口。 +- [x] 5.7 设备批量分配复用导入任务页面,支持 `assign_shop` 和 `assign_series` 两种 operation_type。 +- [x] 5.8 提供两份静态 UTF-8 CSV 模板,校验单列、10MB 和最多 1000 行限制。 + +## 6. Permissions, Errors, and Verification + +- [x] 6.1 为新增页面、按钮和行操作接入最终确认的权限编码。 +- [x] 6.2 统一处理 400、401、403、业务错误、超时和结果未知,不使用底层错误直接作为用户文案。 +- [x] 6.3 验证代理数据隔离、平台权限、敏感字段脱敏和财务操作审计入口。 +- [ ] 6.4 完成后台页面的接口联调、权限组合、空数据、错误、并发冲突和任务恢复验收。 + +## 7. Explicitly Out Of Scope + +- H5/C 端登录限制、实名流程、支付按钮、资产详情、订单续费和 C 端通知不在本次实现任务内。 +- H5 首页隐藏设备下 ICCID 仍按最新说明标记为不做。 +- H5/C 端相关接口仅作为后台联调依赖记录,不在本变更中修改页面或调用代码。 diff --git a/openspec/changes/update-unified-async-task-interaction/tasks.md b/openspec/changes/update-unified-async-task-interaction/tasks.md index 726d1ae..33422da 100644 --- a/openspec/changes/update-unified-async-task-interaction/tasks.md +++ b/openspec/changes/update-unified-async-task-interaction/tasks.md @@ -1,33 +1,33 @@ ## 1. Contract And Model -- [ ] 1.1 确认设备批量分配、批量订购和导出任务创建响应均返回稳定的 `task_id`。 -- [ ] 1.2 确认三类任务按 `task_id` 查询详情的接口路径、状态字段和失败明细字段。 +- [x] 1.1 确认设备批量分配、批量订购和导出任务创建响应均返回稳定的 `task_id`。 +- [x] 1.2 确认三类任务按 `task_id` 查询详情的接口路径、状态字段和失败明细字段。 - [x] 1.3 新增统一任务状态、进度、错误摘要和任务恢复类型,兼容各业务字段别名。 - [x] 1.4 更新导出任务列表和详情类型,补充 `task_id`、`total_count`、`success_count`、`failed_count`、`error_code`、`error_summary`、`updated_at`。 ## 2. Shared Interaction -- [ ] 2.1 实现统一加载中、空数据、无权限、接口失败和重试状态的展示规则。 +- [x] 2.1 实现统一加载中、空数据、无权限、接口失败和重试状态的展示规则。 - [x] 2.2 实现五态任务状态映射,并明确终态为已完成、已失败或已取消。 -- [ ] 2.3 实现统一任务进度展示,支持总数、成功数、失败数和失败明细。 -- [ ] 2.4 实现任务创建后的 `task_id` 持久化、刷新恢复和失效任务清理。 +- [x] 2.3 实现统一任务进度展示,支持总数、成功数、失败数和失败明细。 +- [x] 2.4 实现任务创建后的 `task_id` 持久化、刷新恢复和失效任务清理。 - [x] 2.5 实现 2 秒、3 秒、5 秒递增且最大 10 秒的轮询调度。 - [x] 2.6 实现页面不可见暂停轮询、恢复可见立即刷新和组件卸载清理。 ## 3. Business Pages -- [ ] 3.1 接入设备批量分配页面,创建成功后进入统一任务交互流程。 -- [ ] 3.2 接入批量订购页面,创建成功后进入统一任务交互流程。 +- [x] 3.1 接入设备批量分配页面,创建成功后进入统一任务交互流程。 +- [x] 3.2 接入批量订购页面,创建成功后进入统一任务交互流程。 - [x] 3.3 接入导出任务列表和详情页面,统一展示任务进度、失败摘要和更新时间。 -- [ ] 3.4 保证部分成功任务仍展示为已完成,并显示成功数与失败数,不引入新状态。 -- [ ] 3.5 保证页面刷新或重新进入时恢复任务,不重复提交创建请求。 +- [x] 3.4 保证部分成功任务仍展示为已完成,并显示成功数与失败数,不引入新状态。 +- [x] 3.5 保证页面刷新或重新进入时恢复任务,不重复提交创建请求。 ## 4. Error And Permission Handling - [x] 4.1 403 响应展示无权限状态并停止自动重试。 - [x] 4.2 瞬时接口失败保留已有任务数据和用户输入,并提供明确的手动重试入口。 - [x] 4.3 任务失败优先展示 `error_summary`,不直接展示 `error_code` 或底层技术错误。 -- [ ] 4.4 验证无权限用户无法看到或触发对应批量操作、任务详情和重试操作。 +- [x] 4.4 验证无权限用户无法看到或触发对应批量操作、任务详情和重试操作。 ## 5. Verification @@ -39,6 +39,6 @@ ## Implementation Notes -- 设备批量分配当前仍由 `/api/admin/devices/allocate` 同步返回结果,未返回 `task_id`,因此未接入异步轮询。 -- 当前订单页面仅提供单笔订单创建,仓库中未发现批量订购创建接口或任务详情接口,因此未新增虚构的批量任务流程。 -- 设备批量分配和批量订购需待后端补充异步任务创建及详情契约后继续实施 1.1、1.2、3.1、3.2、3.4、3.5、4.4 及相关测试。 +- 设备批量分配使用 `/api/admin/devices/import/allocations` 创建任务,并复用设备任务详情接口及统一轮询恢复逻辑。 +- 批量订购使用文件 key 创建任务,创建成功后通过 `task_id` 路由参数和持久化状态恢复任务详情。 +- 真实后端环境下的五态、权限和并发回归测试仍需在联调环境执行。 diff --git a/public/templates/bulk-purchase-template.csv b/public/templates/bulk-purchase-template.csv new file mode 100644 index 0000000..49279b8 --- /dev/null +++ b/public/templates/bulk-purchase-template.csv @@ -0,0 +1 @@ +资产标识 diff --git a/public/templates/device-batch-allocation-template.csv b/public/templates/device-batch-allocation-template.csv new file mode 100644 index 0000000..26a6c5d --- /dev/null +++ b/public/templates/device-batch-allocation-template.csv @@ -0,0 +1 @@ +设备标识 diff --git a/src/api/modules/account.ts b/src/api/modules/account.ts index 8b8a39f..9f6a573 100644 --- a/src/api/modules/account.ts +++ b/src/api/modules/account.ts @@ -8,10 +8,14 @@ import type { AccountQueryParams, CreatePlatformAccountParams, BaseResponse, - PaginationResponse + PaginationResponse, + WecomAccountBindingRequest } from '@/types/api' export class AccountService extends BaseService { + static bindWecom(id: number, data: WecomAccountBindingRequest): Promise { + return this.put(`/api/admin/accounts/${id}/wecom-binding`, data) + } // ========== 账号管理 (Account Management) ========== /** diff --git a/src/api/modules/bulkPurchase.ts b/src/api/modules/bulkPurchase.ts index 60b58fa..4c7622c 100644 --- a/src/api/modules/bulkPurchase.ts +++ b/src/api/modules/bulkPurchase.ts @@ -1,31 +1,33 @@ import request from '@/utils/http' import type { + BulkPurchaseCreateRequest, BulkPurchaseCreateApiResponse, - BulkPurchaseItemsApiResponse, + BulkPurchaseTaskListApiResponse, BulkPurchaseTaskApiResponse } from '@/types/api' export class BulkPurchaseService { - static createTask(data: FormData): Promise { + static createTask(data: BulkPurchaseCreateRequest): Promise { return request.post({ - url: '/api/admin/bulk-purchases', + url: '/api/admin/asset-package-batch-orders', data }) } + static getTasks(params?: { + page?: number + page_size?: number + status?: number + }): Promise { + return request.get({ + url: '/api/admin/asset-package-batch-orders', + params + }) + } + static getTask(taskId: number): Promise { return request.get({ - url: `/api/admin/bulk-purchases/${taskId}` - }) - } - - static getItems( - taskId: number, - params?: { page?: number; size?: number; status?: string | number } - ): Promise { - return request.get({ - url: `/api/admin/bulk-purchases/${taskId}/items`, - params + url: `/api/admin/asset-package-batch-orders/${taskId}` }) } } diff --git a/src/api/modules/card.ts b/src/api/modules/card.ts index 2539f29..cd1adfd 100644 --- a/src/api/modules/card.ts +++ b/src/api/modules/card.ts @@ -36,7 +36,10 @@ import type { AssetAllocationRecordDetail, BatchSetCardSeriesBindingRequest, BatchSetCardSeriesBindingResponse, - BatchUpdateAssetRealnamePolicyRequest + BatchUpdateAssetRealnamePolicyRequest, + BatchUpdateAssetRealnamePolicyResponse, + SpeedTierCode, + SetSpeedTierResponse } from '@/types/api' type ApiQueryParams = PaginationParams & Record @@ -57,6 +60,15 @@ interface CardChangeNotice { } export class CardService extends BaseService { + static setSpeedTier( + iccid: string, + code: SpeedTierCode + ): Promise> { + return this.put>( + `/api/admin/iot-cards/${encodeURIComponent(iccid)}/speed-tier`, + { code } + ) + } // ========== 号卡商品管理 ========== /** @@ -418,8 +430,8 @@ export class CardService extends BaseService { static batchUpdateRealnamePolicy( data: BatchUpdateAssetRealnamePolicyRequest, config?: Record - ): Promise> { - return this.post>( + ): Promise> { + return this.post>( '/api/admin/iot-cards/batch-update-realname-policy', data, config diff --git a/src/api/modules/device.ts b/src/api/modules/device.ts index e046c69..e014b2b 100644 --- a/src/api/modules/device.ts +++ b/src/api/modules/device.ts @@ -18,13 +18,15 @@ import type { BatchSetDeviceSeriesBindingRequest, BatchSetDeviceSeriesBindingResponse, BatchUpdateAssetRealnamePolicyRequest, + BatchUpdateAssetRealnamePolicyResponse, ImportDeviceRequest, ImportDeviceResponse, DeviceImportTaskQueryParams, DeviceImportTaskListResponse, DeviceImportTaskDetail, + DeviceBatchAllocationRequest, + DeviceBatchAllocationResponse, BaseResponse, - SetSpeedLimitRequest, SwitchCardRequest, SetWiFiRequest, DeviceOperationResponse @@ -147,6 +149,15 @@ export class DeviceService extends BaseService { ) } + static createAllocationTask( + data: DeviceBatchAllocationRequest + ): Promise> { + return this.post>( + '/api/admin/devices/import/allocations', + data + ) + } + /** * 获取导入任务详情 * @param id 任务ID @@ -176,8 +187,8 @@ export class DeviceService extends BaseService { static batchUpdateRealnamePolicy( data: BatchUpdateAssetRealnamePolicyRequest, config?: Record - ): Promise> { - return this.post>( + ): Promise> { + return this.post>( '/api/admin/devices/batch-update-realname-policy', data, config @@ -208,21 +219,6 @@ export class DeviceService extends BaseService { ) } - /** - * 设置限速 - * @param imei 设备号(IMEI) - * @param data 限速参数 - */ - static setSpeedLimit( - imei: string, - data: SetSpeedLimitRequest - ): Promise> { - return this.put>( - `/api/admin/devices/by-identifier/${imei}/speed-limit`, - data - ) - } - /** * 切换SIM卡 * @param imei 设备号(IMEI) diff --git a/src/api/modules/exchange.ts b/src/api/modules/exchange.ts index f9d5868..f2e5b00 100644 --- a/src/api/modules/exchange.ts +++ b/src/api/modules/exchange.ts @@ -5,12 +5,16 @@ import { BaseService } from '../BaseService' import type { BaseResponse } from '@/types/api' +export type ExchangeAssetType = 'iot_card' | 'device' +export type ExchangeFlowType = 'shipping' | 'direct' +export type ExchangeStatus = 1 | 2 | 3 | 4 | 5 + // 换货单查询参数 export interface ExchangeQueryParams { page?: number page_size?: number - status?: number // 换货状态 - flow_type?: string // 流程类型(shipping/direct) + status?: ExchangeStatus // 换货状态 + flow_type?: ExchangeFlowType // 流程类型(shipping/direct) old_asset_keyword?: string // 旧资产关键词(ICCID、接入号、虚拟号、IMEI、SN) new_asset_keyword?: string // 新资产关键词(ICCID、接入号、虚拟号、IMEI、SN) created_at_start?: string // 创建时间起始 @@ -20,9 +24,9 @@ export interface ExchangeQueryParams { // 创建换货单请求 export interface CreateExchangeRequest { exchange_reason: string // 换货原因 - old_asset_type: string // 旧资产类型 (iot_card 或 device) + old_asset_type: ExchangeAssetType // 旧资产类型 (iot_card 或 device) old_identifier: string // 旧资产标识符(ICCID/虚拟号/IMEI/SN) - flow_type?: string // 流程类型(shipping/direct),默认 shipping + flow_type?: ExchangeFlowType // 流程类型(shipping/direct),默认 shipping new_identifier?: string // 新资产标识符(direct 时必填) migrate_data?: boolean // 是否迁移数据(默认 false) remark?: string // 备注(可选) @@ -33,16 +37,16 @@ export interface ExchangeResponse { id: number exchange_no: string exchange_reason: string - old_asset_type: string + old_asset_type: ExchangeAssetType old_asset_id: number old_asset_identifier: string - new_asset_type?: string | null + new_asset_type?: ExchangeAssetType | null new_asset_id?: number | null new_asset_identifier?: string | null - status: number // 换货状态(1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消) + status: ExchangeStatus // 换货状态(1:待填写信息, 2:待发货, 3:已发货待确认, 4:已完成, 5:已取消) status_name?: string status_text: string - flow_type: string // 流程类型(shipping/direct) + flow_type: ExchangeFlowType // 流程类型(shipping/direct) flow_type_name: string // 流程类型名称 shipped_at?: string | null // 发货时间(仅 shipping 发货后有值) completed_at?: string | null // 换货完成时间 diff --git a/src/api/modules/exportTask.ts b/src/api/modules/exportTask.ts index e6633a8..db2dee1 100644 --- a/src/api/modules/exportTask.ts +++ b/src/api/modules/exportTask.ts @@ -19,9 +19,10 @@ export class ExportTaskService extends BaseService { } static getExportTaskDetail(id: number): Promise { - return this.getOne(`/api/admin/export-tasks/${id}`) + return this.get(`/api/admin/export-tasks/${id}`) } + /** 保留旧页面的取消入口,后端支持时由页面权限控制显示。 */ static cancelExportTask(id: number): Promise { return this.post(`/api/admin/export-tasks/${id}/cancel`, {}) } diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts index 2601d94..b5e6ccc 100644 --- a/src/api/modules/index.ts +++ b/src/api/modules/index.ts @@ -37,6 +37,7 @@ export { ExportTaskService } from './exportTask' export { OrderPackageInvalidateTaskService } from './orderPackageInvalidateTask' export { BulkPurchaseService } from './bulkPurchase' export { NotificationService } from './notification' +export { WecomService } from './wecom' // TODO: 按需添加其他业务模块 // export { SettingService } from './setting' diff --git a/src/api/modules/packageManage.ts b/src/api/modules/packageManage.ts index 77004f9..9d5f629 100644 --- a/src/api/modules/packageManage.ts +++ b/src/api/modules/packageManage.ts @@ -11,7 +11,7 @@ import type { UpdatePackageStatusRequest, UpdatePackageShelfStatusRequest, BaseResponse, - PaginationResponse, + PaginationResponse } from '@/types/api' export class PackageManageService extends BaseService { @@ -68,7 +68,7 @@ export class PackageManageService extends BaseService { * 更新套餐状态 * PUT /api/admin/packages/{id}/status * @param id 套餐ID - * @param status 状态 (1:启用, 2:禁用) + * @param status 状态 (0:禁用, 1:启用) */ static updatePackageStatus(id: number, status: number): Promise { const data: UpdatePackageStatusRequest = { status } diff --git a/src/api/modules/storage.ts b/src/api/modules/storage.ts index 077ee8a..456481d 100644 --- a/src/api/modules/storage.ts +++ b/src/api/modules/storage.ts @@ -23,7 +23,12 @@ const triggerBrowserDownload = (downloadUrl: string, fileName: string) => { /** * 文件用途枚举 */ -export type FilePurpose = 'iot_import' | 'device_import' | 'export' | 'attachment' +export type FilePurpose = + | 'iot_import' + | 'batch_purchase' + | 'device_batch_allocation' + | 'export' + | 'attachment' /** * 获取上传 URL 请求参数 diff --git a/src/api/modules/wecom.ts b/src/api/modules/wecom.ts new file mode 100644 index 0000000..7893e7d --- /dev/null +++ b/src/api/modules/wecom.ts @@ -0,0 +1,78 @@ +import { BaseService } from '../BaseService' +import type { + BaseResponse, + WecomAccountBindingRequest, + WecomApplication, + WecomApplicationListResponse, + WecomApplicationQueryParams, + WecomApplicationRequest, + WecomApplicationResponse, + WecomBusinessType, + WecomMemberListResponse, + WecomMemberQueryParams, + WecomSceneListResponse, + WecomSceneQueryParams, + WecomSceneRequest, + WecomSceneResponse, + WecomSyncMembersApiResponse +} from '@/types/api' + +export class WecomService extends BaseService { + static getApplications( + params?: WecomApplicationQueryParams + ): Promise { + return this.get('/api/admin/wecom/applications', params) + } + + static saveApplication(data: WecomApplicationRequest): Promise { + return this.post('/api/admin/wecom/applications', data) + } + + static testApplication(id: number): Promise> { + return this.post>(`/api/admin/wecom/applications/${id}/test`) + } + + static syncMembers(id: number): Promise { + return this.post( + `/api/admin/wecom/applications/${id}/members/sync` + ) + } + + static getMembers( + id: number, + params?: WecomMemberQueryParams + ): Promise { + return this.get( + `/api/admin/wecom/applications/${id}/members`, + params + ) + } + + static setDefaultCreator(id: number, userid: string): Promise { + return this.put( + `/api/admin/wecom/applications/${id}/default-creator`, + { userid } + ) + } + + static getScenes(params?: WecomSceneQueryParams): Promise { + return this.get('/api/admin/wecom/scenes', params) + } + + static saveScene( + businessType: WecomBusinessType, + data: WecomSceneRequest + ): Promise { + return this.put(`/api/admin/wecom/scenes/${businessType}`, data) + } + + static bindAccount( + accountId: number, + data: WecomAccountBindingRequest + ): Promise> { + return this.put>( + `/api/admin/accounts/${accountId}/wecom-binding`, + data + ) + } +} diff --git a/src/components/business/VoucherUpload.vue b/src/components/business/VoucherUpload.vue index 898f154..0634682 100644 --- a/src/components/business/VoucherUpload.vue +++ b/src/components/business/VoucherUpload.vue @@ -57,7 +57,8 @@ import { Close, Document, UploadFilled } from '@element-plus/icons-vue' import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus' import { StorageService } from '@/api/modules' - import type { RefundAttachment } from '@/types/api/refund' + import type { RefundAttachment } from '@/types/api/refund' + import type { FilePurpose } from '@/api/modules/storage' interface Props { modelValue?: string[] | string @@ -65,13 +66,21 @@ maxCount?: number accept?: string tip?: string + purpose?: FilePurpose + maxSizeMb?: number + singleColumnCsv?: boolean + maxCsvRows?: number } const props = withDefaults(defineProps(), { voucherName: '凭证', maxCount: 5, accept: '', - tip: '' + tip: '', + purpose: 'attachment', + maxSizeMb: 0, + singleColumnCsv: false, + maxCsvRows: 0 }) const emit = defineEmits<{ @@ -185,6 +194,27 @@ const file = uploadFile.raw if (!file) return + if (props.maxSizeMb > 0 && file.size > props.maxSizeMb * 1024 * 1024) { + ElMessage.warning(`文件不能超过 ${props.maxSizeMb}MB`) + removeUploadFile(uploadFile) + return + } + + if (props.singleColumnCsv) { + const content = await file.text() + const rows = content.replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean) + if (props.maxCsvRows > 0 && Math.max(rows.length - 1, 0) > props.maxCsvRows) { + ElMessage.warning(`CSV 数据行不能超过 ${props.maxCsvRows} 行`) + removeUploadFile(uploadFile) + return + } + if (rows.some((row) => row.includes(','))) { + ElMessage.warning('CSV 只能包含一列资产标识') + removeUploadFile(uploadFile) + return + } + } + if (props.accept) { const accepted = props.accept.split(',').some((type) => { const value = type.trim().toLowerCase() @@ -220,7 +250,7 @@ const uploadUrlRes = await StorageService.getUploadUrl({ file_name: file.name, content_type: contentType, - purpose: 'attachment' + purpose: props.purpose }) if (uploadUrlRes.code !== 0) { diff --git a/src/composables/useAsyncTaskPolling.ts b/src/composables/useAsyncTaskPolling.ts index 6999e54..432509c 100644 --- a/src/composables/useAsyncTaskPolling.ts +++ b/src/composables/useAsyncTaskPolling.ts @@ -8,6 +8,7 @@ import { type ShallowRef } from 'vue' import { isAsyncTaskActive, isAsyncTaskTerminal, type AsyncTaskProgress } from '@/types/api' +import { normalizeApiError } from '@/utils/business/apiError' const POLL_DELAYS = [2000, 3000, 5000] const MAX_POLL_DELAY = 10000 @@ -50,8 +51,7 @@ const writeStoredTaskId = (storageKey: string, taskId: number | null) => { } } -const getErrorMessage = (error: any) => - error?.response?.data?.msg || error?.message || '获取导出任务详情失败' +const getErrorMessage = (error: unknown) => normalizeApiError(error).message export function useAsyncTaskPolling( options: AsyncTaskPollingOptions diff --git a/src/config/constants/exportTask.ts b/src/config/constants/exportTask.ts index 41cd220..66655e8 100644 --- a/src/config/constants/exportTask.ts +++ b/src/config/constants/exportTask.ts @@ -43,6 +43,56 @@ export const EXPORT_TASK_SCENE_CONFIG: Record - error_code?: string error_summary?: string - created_at?: string - updated_at?: string - started_at?: string - completed_at?: string + error_message: string + creator_name: string + voucher_keys: string[] | null + started_at: string | null + completed_at: string | null + created_at: string + updated_at: string + items?: BulkPurchaseItem[] } export interface BulkPurchaseItem { - id?: number - row_number?: number - line?: number - asset_identifier?: string + line: number + asset_identifier: string + package_code?: string + status: BulkPurchaseTaskStatus + status_name: string + order_id: number + order_no: string + amount: number + reason: string iccid?: string virtual_no?: string - package_code?: string - status?: string | number - status_name?: string - error_code?: string error_reason?: string error_summary?: string } @@ -64,4 +80,11 @@ export interface BulkPurchaseItemsResponse { export type BulkPurchaseCreateApiResponse = BaseResponse export type BulkPurchaseTaskApiResponse = BaseResponse -export type BulkPurchaseItemsApiResponse = BaseResponse +export interface BulkPurchaseTaskListResponse { + items: BulkPurchaseTask[] + page: number + size: number + total: number +} + +export type BulkPurchaseTaskListApiResponse = BaseResponse diff --git a/src/types/api/card.ts b/src/types/api/card.ts index e282a94..4e207af 100644 --- a/src/types/api/card.ts +++ b/src/types/api/card.ts @@ -5,6 +5,16 @@ import { PaginationParams, ImportTask } from './common' import type { ExpiryEstimateStatus } from './asset' +export type SpeedTierCode = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 + +export interface SetSpeedTierResponse { + code: SpeedTierCode + iccid: string + integration_id: string + iot_card_id: number + speed_tier_name: string +} + // 运营商类型 export enum Operator { CHINA_MOBILE = 'mobile', // 中国移动 diff --git a/src/types/api/device.ts b/src/types/api/device.ts index 6d2f941..7273d88 100644 --- a/src/types/api/device.ts +++ b/src/types/api/device.ts @@ -3,7 +3,7 @@ */ import { PaginationParams } from './common' -import type { ExpiryEstimateStatus } from './asset' +import type { AssetRealnamePolicy, ExpiryEstimateStatus } from './asset' // ========== 设备状态枚举 ========== @@ -65,7 +65,7 @@ export interface Device { last_gateway_sync_at?: string | null // 最后 sync-info 同步时间 imei?: string // IMEI(设备国际移动设备识别码) sn?: string // 设备序列号 - realname_policy?: string // 实名认证策略 + realname_policy?: AssetRealnamePolicy // 实名认证策略 real_name_status: 0 | 1 // 实名状态 (0:未实名, 1:已实名) real_name_status_name: string // 实名状态名称 estimated_final_expires_at?: string | null // 当前及排队主套餐接续后的预计最终到期时间 @@ -217,6 +217,7 @@ export enum DeviceImportTaskStatus { // 导入任务查询参数 export interface DeviceImportTaskQueryParams extends PaginationParams { status?: DeviceImportTaskStatus // 任务状态 + operation_type?: 'import' | 'assign_shop' | 'assign_series' batch_no?: string // 批次号(模糊查询) start_time?: string // 创建时间起始 end_time?: string // 创建时间结束 @@ -237,6 +238,11 @@ export interface DeviceImportTask { fail_count: number // 失败数 skip_count: number // 跳过数 error_message: string // 错误信息 + operation_type: 'import' | 'assign_shop' | 'assign_series' + operation_name: string + target_id: number | null + realname_policy: 'none' | 'before_order' | 'after_order' + warning_count: number created_at: string // 创建时间 started_at: string | null // 开始处理时间 completed_at: string | null // 完成时间 @@ -244,17 +250,17 @@ export interface DeviceImportTask { // 导入任务列表响应 export interface DeviceImportTaskListResponse { - list: DeviceImportTask[] | null // 任务列表 + items: DeviceImportTask[] // 任务列表 page: number // 当前页码 - page_size: number // 每页数量 + size: number // 每页数量 total: number // 总数 - total_pages: number // 总页数 } // 导入结果详细项 export interface DeviceImportResultItem { line: number // 行号 - virtual_no: string // 虚拟号(原 device_no) + device_identifier: string // CSV 原始设备标识 + virtual_no: string // 设备虚拟号 reason: string // 原因 } @@ -262,6 +268,19 @@ export interface DeviceImportResultItem { export interface DeviceImportTaskDetail extends DeviceImportTask { failed_items: DeviceImportResultItem[] | null // 失败记录详情 skipped_items: DeviceImportResultItem[] | null // 跳过记录详情 + warning_items: DeviceImportResultItem[] | null // 警告记录详情 +} + +export interface DeviceBatchAllocationRequest { + file_key: string + operation_type: 'assign_shop' | 'assign_series' + target_id: number +} + +export interface DeviceBatchAllocationResponse { + message: string + task_id: number + task_no: string } // ========== 批量设置设备的套餐系列绑定相关 ========== diff --git a/src/types/api/exportTask.ts b/src/types/api/exportTask.ts index 00f193c..451d746 100644 --- a/src/types/api/exportTask.ts +++ b/src/types/api/exportTask.ts @@ -1,6 +1,14 @@ import type { BaseResponse, PaginationParams } from './common' -export type ExportTaskScene = 'device' | 'iot_card' | 'order' +export type ExportTaskScene = + | 'device' + | 'iot_card' + | 'order' + | 'package' + | 'agent_wallet_transaction' + | 'agent_recharge' + | 'refund' + | 'exchange' export type ExportTaskFormat = 'xlsx' | 'csv' @@ -45,6 +53,10 @@ export interface ExportTaskItem { created_at: string started_at?: string completed_at?: string + creator_enterprise_id?: number | null + creator_shop_id?: number | null + creator_user_id?: number + creator_user_type?: number } export interface ExportTaskListResponse { diff --git a/src/types/api/index.ts b/src/types/api/index.ts index 31b36ac..7455c26 100644 --- a/src/types/api/index.ts +++ b/src/types/api/index.ts @@ -125,3 +125,6 @@ export * from './bulkPurchase' // 站内通知相关 export * from './notification' + +// 企业微信审批配置相关 +export * from './wecom' diff --git a/src/types/api/order.ts b/src/types/api/order.ts index 169b97c..abc92c6 100644 --- a/src/types/api/order.ts +++ b/src/types/api/order.ts @@ -22,6 +22,14 @@ export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay' | 'offline' // 订单操作者类型 export type OrderOperatorType = 'platform' | 'agent' | 'enterprise' | 'personal_customer' +export type PurchaseRole = + | 'self_purchase' + | 'purchased_by_parent' + | 'purchased_by_platform' + | 'purchase_for_subordinate' + +export type OrderAssetType = 'card' | 'device' + // 订单佣金流程状态 export enum OrderCommissionStatus { PENDING = 1, // 待计算 @@ -71,9 +79,10 @@ export interface Order { device_id: number | null iot_card_id: number | null asset_identifier?: string // 资产标识符快照(ICCID 或 VirtualNo) - asset_type?: string // 资产类型:single_card 或 device - purchase_role?: string // 订单渠道 + asset_type?: OrderAssetType // 资产类型:card 或 device + purchase_role?: PurchaseRole // 订单渠道 purchase_remark?: string // 购买备注 + is_purchase_on_behalf?: boolean // 是否为代购订单 is_purchased_by_parent?: boolean // 是否由上级代购 is_expired?: boolean // 是否已过期 expires_at?: string | null // 订单超时时间 @@ -97,13 +106,13 @@ export interface OrderQueryParams { order_type?: OrderType order_no?: string identifier?: string // 按资产标识符过滤(ICCID 或 VirtualNo) - purchase_role?: string // 订单渠道 + purchase_role?: PurchaseRole // 订单渠道 buyer_phone?: string // 按买家手机号精确过滤 start_time?: string end_time?: string is_expired?: boolean // 是否已过期 seller_shop_id?: number // 所属代理商ID(销售来源店铺ID) - dateRange?: string[] | any // For date range picker in UI + dateRange?: string[] // For date range picker in UI } // 订单列表响应 diff --git a/src/types/api/packageManagement.ts b/src/types/api/packageManagement.ts index 8561bc3..44cd49d 100644 --- a/src/types/api/packageManagement.ts +++ b/src/types/api/packageManagement.ts @@ -171,7 +171,7 @@ export interface PackageResponse extends PackageAllocationExpiryBaseFields { suggested_retail_price?: number | null // 建议零售价(分),可能为 null 表示"未配置" retail_price?: number | null // 原始代理零售价(分),可能为 null is_gift?: boolean // 是否为赠送套餐 - price_config_status?: string // 价格配置状态 + price_config_status?: number // 价格配置状态:0未配置,1赠送0价,2已配置非0 price_config_status_name?: string // 价格配置状态名称 effective_retail_price?: number | null // 有效零售价(分) retail_price_config_status?: string // 零售价配置状态(代理视角) @@ -180,7 +180,7 @@ export interface PackageResponse extends PackageAllocationExpiryBaseFields { profit_margin?: number | null // 利润空间(分,仅代理用户可见) tier_info?: CommissionTierInfo // 返佣档位信息 shelf_status?: number // 上架状态 (1:上架, 2:下架) - status?: number // 状态 (1:启用, 2:禁用) + status?: number // 状态 (0:禁用, 1:启用) description?: string created_at?: string updated_at?: string @@ -193,7 +193,6 @@ export interface PackageQueryParams extends PaginationParams { package_name?: string // 套餐名称(模糊搜索) series_id?: number // 系列ID package_type?: string // 套餐类型 - data_type?: string // 流量类型 shelf_status?: number // 上架状态 status?: number // 状态 } @@ -244,7 +243,7 @@ export interface UpdatePackageRequest { * 更新套餐状态请求 */ export interface UpdatePackageStatusRequest { - status: number // 1:启用, 2:禁用 + status: number // 0:禁用, 1:启用 } /** diff --git a/src/types/api/refund.ts b/src/types/api/refund.ts index 5ad49ea..044e3df 100644 --- a/src/types/api/refund.ts +++ b/src/types/api/refund.ts @@ -66,8 +66,11 @@ export interface Refund { asset_reset: boolean commission_deducted: boolean submitter_name?: string | null // 提交人名称 + submitter_id?: number | null + approval_provider?: 'wecom' | null + approval_instance_id?: number | null approval_source?: 'none' | 'legacy' | 'wecom' | null // 审批来源 - approval_status?: string | null // 审批状态 + approval_status?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null // 审批状态 approval_status_name?: string | null // 审批状态名称 current_approver_summary?: string | null // 当前审批人摘要 processing_status?: string | null // 业务处理状态 diff --git a/src/types/api/shop.ts b/src/types/api/shop.ts index 6fc471c..13d7cdf 100644 --- a/src/types/api/shop.ts +++ b/src/types/api/shop.ts @@ -26,6 +26,7 @@ export interface ShopResponse { business_owner_username: string // 平台业务员账号名 business_owner_phone_summary: string // 平台业务员手机号摘要 business_owner_available: boolean // 是否仍可用于通知接收 + client_login_disabled: boolean // 是否禁止 C 端新登录 } // 店铺列表查询参数 @@ -39,6 +40,7 @@ export interface ShopQueryParams extends PaginationParams { status?: number | null // 状态 (0:禁用, 1:启用) contact_phone?: string // 联系电话精确查询 business_owner_account_id?: number | null // 平台业务员账号ID + client_login_disabled?: boolean page?: number // 页码 page_size?: number // 每页数量 } @@ -59,6 +61,7 @@ export interface CreateShopParams { contact_name?: string // 联系人姓名 contact_phone?: string // 联系人电话 business_owner_account_id?: number | null // 平台业务员账号ID + client_login_disabled?: boolean } // 更新店铺参数 @@ -72,6 +75,7 @@ export interface UpdateShopParams { contact_name?: string // 联系人姓名 contact_phone?: string // 联系人电话 business_owner_account_id?: number | null // 平台业务员账号ID + client_login_disabled?: boolean } // 店铺业务员候选 diff --git a/src/types/api/systemConfig.ts b/src/types/api/systemConfig.ts index 2a0b7e9..645484c 100644 --- a/src/types/api/systemConfig.ts +++ b/src/types/api/systemConfig.ts @@ -2,6 +2,12 @@ import type { PaginationParams } from './common' export type SystemConfigModule = 'carrier_callback' | 'c2b.payment' +export type AllowedPaymentMethod = 'wallet' | 'wechat' | 'alipay' +export const PAYMENT_CONFIG_KEYS = { + card: 'c2b.payment.card_allowed_methods', + device: 'c2b.payment.device_allowed_methods' +} as const + export type SystemConfigValueType = 'string' | 'int' | 'bool' | 'json' export interface SystemConfigItem { @@ -35,3 +41,16 @@ export interface UpdateSystemConfigRequest { key: string value: string } + +export function parsePaymentMethods(value: string): AllowedPaymentMethod[] { + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) + ? parsed.filter((method): method is AllowedPaymentMethod => + ['wallet', 'wechat', 'alipay'].includes(method) + ) + : [] + } catch { + return [] + } +} diff --git a/src/types/api/wecom.ts b/src/types/api/wecom.ts new file mode 100644 index 0000000..fd9ce76 --- /dev/null +++ b/src/types/api/wecom.ts @@ -0,0 +1,95 @@ +import type { BaseResponse, PaginationData, PaginationParams } from './common' + +export type WecomBusinessType = 'refund_approval' | 'offline_recharge_approval' + +export interface WecomApplication { + id: number + corp_id: string + agent_id: number + name: string + secret?: string + callback_token?: string + encoding_aes_key?: string + credentials_set: boolean + default_creator_userid?: string | null + default_creator_name?: string | null + status: 0 | 1 + status_name: string + last_connected_at: string | null + created_at: string + updated_at: string +} + +export interface WecomApplicationRequest { + corp_id?: string + agent_id?: number + name?: string + secret?: string + callback_token?: string + encoding_aes_key?: string + status?: 0 | 1 +} + +export interface WecomMember { + application_id: number + corp_id: string + userid: string + name: string + department_ids: number[] | null + synced_at: string +} + +export interface WecomSceneControlMapping { + business_field: string + control_id: string + control_type: string + option_mapping: Record +} + +export interface WecomScene { + id: number + application_id: number + business_type: WecomBusinessType + business_type_name: string + template_id: string + template_name: string + control_mapping: WecomSceneControlMapping[] + template_fingerprint: string + last_verified_at: string + status: 0 | 1 + status_name: string + updated_at: string +} + +export interface WecomSceneRequest { + application_id?: number + template_id?: string + control_mapping?: WecomSceneControlMapping[] + status?: 0 | 1 +} + +export interface WecomAccountBindingRequest { + application_id: number + userid: string +} + +export interface WecomSyncMembersResponse { + application_id: number + synced_at: string + synced_count: number +} + +export type WecomApplicationListResponse = BaseResponse> +export type WecomMemberListResponse = BaseResponse> +export type WecomSceneListResponse = BaseResponse> +export type WecomApplicationResponse = BaseResponse +export type WecomSceneResponse = BaseResponse +export type WecomSyncMembersApiResponse = BaseResponse + +export interface WecomMemberQueryParams extends PaginationParams { + keyword?: string +} + +export interface WecomApplicationQueryParams extends PaginationParams {} + +export interface WecomSceneQueryParams extends PaginationParams {} diff --git a/src/types/components.d.ts b/src/types/components.d.ts index ca21b11..e898e2d 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -91,6 +91,7 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCascader: typeof import('element-plus/es')['ElCascader'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCol: typeof import('element-plus/es')['ElCol'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] diff --git a/src/utils/business/apiError.ts b/src/utils/business/apiError.ts new file mode 100644 index 0000000..bbb0129 --- /dev/null +++ b/src/utils/business/apiError.ts @@ -0,0 +1,31 @@ +import axios from 'axios' + +export type ApiErrorKind = 'auth' | 'forbidden' | 'validation' | 'conflict' | 'timeout' | 'server' | 'unknown' + +export interface NormalizedApiError { + kind: ApiErrorKind + status?: number + code?: number | string + message: string + unknownResult: boolean +} + +const getResponse = (error: unknown) => (axios.isAxiosError(error) ? error.response : undefined) + +export const normalizeApiError = (error: unknown): NormalizedApiError => { + const response = getResponse(error) + const status = response?.status + const code = response?.data?.code + const backendMessage = response?.data?.msg + const timeout = axios.isAxiosError(error) && error.code === 'ECONNABORTED' + + if (timeout) { + return { kind: 'timeout', status, code, message: '请求超时,结果可能未知,请刷新确认', unknownResult: true } + } + if (status === 401 || code === 401) return { kind: 'auth', status, code, message: '登录状态已失效,请重新登录', unknownResult: false } + if (status === 403) return { kind: 'forbidden', status, code, message: '没有执行此操作的权限', unknownResult: false } + if (status === 400) return { kind: 'validation', status, code, message: backendMessage || '请求参数或业务条件不符合要求', unknownResult: false } + if (status === 409) return { kind: 'conflict', status, code, message: backendMessage || '数据已发生变化,请刷新后重试', unknownResult: false } + if (status && status >= 500) return { kind: 'server', status, code, message: '服务器处理失败,请稍后重试', unknownResult: false } + return { kind: 'unknown', status, code, message: backendMessage || '操作失败,请稍后重试', unknownResult: false } +} diff --git a/src/utils/business/approvalSummary.ts b/src/utils/business/approvalSummary.ts index 3950964..075bc29 100644 --- a/src/utils/business/approvalSummary.ts +++ b/src/utils/business/approvalSummary.ts @@ -2,6 +2,9 @@ export type ApprovalSource = 'none' | 'legacy' | 'wecom' export interface ApprovalSummary { approval_source?: ApprovalSource | null + approval_provider?: 'wecom' | null + approval_instance_id?: number | null + approval_status?: number | string | null approval_status_name?: string | null current_approver_summary?: string | null processing_status_name?: string | null @@ -9,14 +12,36 @@ export interface ApprovalSummary { export const getApprovalStatusText = (summary: ApprovalSummary): string => { if (summary.approval_source === 'legacy') return '历史审批' - if (summary.approval_source === 'wecom') return summary.approval_status_name || '-' + if ( + summary.approval_source === 'wecom' || + summary.approval_provider === 'wecom' || + summary.approval_instance_id + ) { + return summary.approval_status_name || approvalStatusNames[Number(summary.approval_status)] || '-' + } return '-' } export const getCurrentApproverSummaryText = (summary: ApprovalSummary): string => { - if (summary.approval_source !== 'wecom') return '-' + if ( + summary.approval_source !== 'wecom' && + summary.approval_provider !== 'wecom' && + !summary.approval_instance_id + ) return '-' return summary.current_approver_summary || '-' } export const getProcessingStatusText = (summary: ApprovalSummary): string => summary.processing_status_name || '-' + +export const approvalStatusNames: Record = { + 0: '提交中', + 1: '审批中', + 2: '已通过', + 3: '已拒绝', + 4: '已撤销', + 5: '通过后撤销', + 6: '已删除', + 7: '提交失败', + 8: '提交结果未知' +} diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts index 46a0196..8f021cd 100644 --- a/src/utils/http/index.ts +++ b/src/utils/http/index.ts @@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus' import { useUserStore } from '@/store/modules/user' import { ApiStatus } from './status' import type { RequestOptions, ErrorMessageMode } from '@/types/api' +import { normalizeApiError } from '@/utils/business/apiError' const axiosInstance = axios.create({ timeout: 15000, // 请求超时时间(毫秒) @@ -220,7 +221,8 @@ function handleErrorMessage( ) { if (mode === 'none') return - const backendMessage = error.response?.data?.msg + const normalized = normalizeApiError(error) + const backendMessage = normalized.message const httpStatus = error.response?.status // 401 过期类错误由 clearLocalStateAndRedirect 统一提示,避免重复弹出后端报错。 @@ -245,7 +247,7 @@ function handleErrorMessage( } // 其他错误显示通用提示 - const message = '请求超时或服务器异常!' + const message = normalized.message if (mode === 'modal') { // TODO: 可以使用 ElMessageBox 显示模态框 ElMessage.error(message) diff --git a/src/views/asset-management/asset-information/components/BasicInfoCard.vue b/src/views/asset-management/asset-information/components/BasicInfoCard.vue index e5ec523..7cb0949 100644 --- a/src/views/asset-management/asset-information/components/BasicInfoCard.vue +++ b/src/views/asset-management/asset-information/components/BasicInfoCard.vue @@ -9,6 +9,14 @@
+ + 设置限速 +
自动轮询 @@ -194,33 +202,6 @@ - 同步状态 - - - - {{ cardInfo.polling.enabled ? '已启用' : '未启用' }} - - - - - - {{ formatDateTime(cardInfo?.polling?.last_activity_at) || '-' }} - - - - 查看同步轨迹 - - - -