Compare commits
7 Commits
99de03604d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c8524485b | ||
|
|
4c0207c6e6 | ||
|
|
5b8d6610ab | ||
|
|
a036b10641 | ||
|
|
c1a7118d35 | ||
|
|
f27bbacb1e | ||
|
|
acda0d82c8 |
@@ -0,0 +1,37 @@
|
||||
# Change: 更新资产解析流量汇总展示
|
||||
|
||||
## Why
|
||||
|
||||
后台“资产信息”页需要展示当前世代下所有套餐的虚流量已用量和剩余量,方便运营人员在查看资产基础信息时快速判断整体流量消耗情况。
|
||||
|
||||
后端通过 `GET /api/admin/assets/resolve/:identifier` 新增可选查询参数控制是否返回汇总字段;前端需要明确请求契约、类型契约和展示位置,避免默认查询缺失汇总数据。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 更新资产解析接口请求契约:
|
||||
- `GET /api/admin/assets/resolve/:identifier` 支持可选 Query 参数 `include_usage_summary: boolean`
|
||||
- 参数含义为是否返回当前世代流量汇总字段 `total_virtual_used_mb` / `total_virtual_remaining_mb`
|
||||
- 后端未传时默认不返回汇总数据;后台“资产信息”页发起解析请求时默认传 `include_usage_summary=true`
|
||||
- 更新资产解析接口响应契约:
|
||||
- 新增 `total_virtual_used_mb?: number | null`,表示所有套餐已用量(MB),未请求时为 `null`
|
||||
- 新增 `total_virtual_remaining_mb?: number | null`,表示所有套餐剩余量(MB),未请求时为 `null`
|
||||
- 更新后台“资产信息”基础信息展示:
|
||||
- 卡资产在“运营商状态”字段后新增“所有套餐已用量”和“所有套餐剩余量”
|
||||
- 设备资产在“SN”字段后新增“所有套餐已用量”和“所有套餐剩余量”
|
||||
- 字段值使用上述两个响应字段,并按流量大小格式化展示
|
||||
- 字段为空或未请求时显示稳定占位内容
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `asset-information`
|
||||
- Affected code:
|
||||
- `src/api/modules/asset.ts`
|
||||
- `src/types/api/asset.ts`
|
||||
- `src/views/asset-management/asset-information/types.ts`
|
||||
- `src/views/asset-management/asset-information/composables/useAssetInfo.ts`
|
||||
- `src/views/asset-management/asset-information/components/BasicInfoCard.vue`
|
||||
- Dependencies:
|
||||
- `GET /api/admin/assets/resolve/:identifier` 后端支持 `include_usage_summary` Query 参数及响应字段
|
||||
- Breaking changes:
|
||||
- 无;新增请求参数和响应字段均为可选,前端资产信息页仅主动请求并展示新增数据
|
||||
@@ -0,0 +1,60 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Admin Asset Resolve Usage Summary Contract
|
||||
|
||||
The admin asset information integration SHALL request usage summary data from `GET /api/admin/assets/resolve/:identifier` by default and preserve the returned current-generation virtual usage summary fields.
|
||||
|
||||
#### Scenario: Asset information requests usage summary by default
|
||||
|
||||
- **GIVEN** 用户在后台“资产信息”页查询资产标识符
|
||||
- **WHEN** 前端调用 `GET /api/admin/assets/resolve/:identifier`
|
||||
- **THEN** 请求 Query MUST include `include_usage_summary=true`
|
||||
- **AND** `include_usage_summary` MUST be treated as a boolean query parameter
|
||||
|
||||
#### Scenario: Preserve usage summary fields from resolve response
|
||||
|
||||
- **GIVEN** `GET /api/admin/assets/resolve/:identifier?include_usage_summary=true` 返回 `total_virtual_used_mb` 和 `total_virtual_remaining_mb`
|
||||
- **WHEN** 前端解析资产详情响应
|
||||
- **THEN** 页面状态 MUST preserve `total_virtual_used_mb` as a `number | null` field
|
||||
- **AND** 页面状态 MUST preserve `total_virtual_remaining_mb` as a `number | null` field
|
||||
- **AND** `total_virtual_used_mb` MUST represent 所有套餐已用量(MB)
|
||||
- **AND** `total_virtual_remaining_mb` MUST represent 所有套餐剩余量(MB)
|
||||
|
||||
#### Scenario: Usage summary not requested
|
||||
|
||||
- **GIVEN** a caller invokes `GET /api/admin/assets/resolve/:identifier` without `include_usage_summary=true`
|
||||
- **WHEN** the response is parsed
|
||||
- **THEN** `total_virtual_used_mb` MAY be absent or `null`
|
||||
- **AND** `total_virtual_remaining_mb` MAY be absent or `null`
|
||||
|
||||
### Requirement: Admin Asset Usage Summary Display
|
||||
|
||||
The admin asset information view SHALL display current-generation virtual usage summary fields in both card and device asset information sections using asset-type-specific placement.
|
||||
|
||||
#### Scenario: Show card usage summary after carrier status
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** 当前资产详情状态中存在 `total_virtual_used_mb` 和 `total_virtual_remaining_mb`
|
||||
- **WHEN** 页面渲染“卡信息”区域
|
||||
- **THEN** 页面 MUST keep `运营商状态` visible
|
||||
- **AND** 页面 MUST display `所有套餐已用量` immediately after `运营商状态`, using `total_virtual_used_mb`
|
||||
- **AND** 页面 MUST display `所有套餐剩余量` immediately after `所有套餐已用量`, using `total_virtual_remaining_mb`
|
||||
- **AND** both values MUST be formatted as traffic sizes based on MB values
|
||||
|
||||
#### Scenario: Show device usage summary after SN
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的设备资产详情
|
||||
- **AND** 当前资产详情状态中存在 `total_virtual_used_mb` 和 `total_virtual_remaining_mb`
|
||||
- **WHEN** 页面渲染“设备信息”区域
|
||||
- **THEN** 页面 MUST keep `SN` visible
|
||||
- **AND** 页面 MUST display `所有套餐已用量` immediately after `SN`, using `total_virtual_used_mb`
|
||||
- **AND** 页面 MUST display `所有套餐剩余量` immediately after `所有套餐已用量`, using `total_virtual_remaining_mb`
|
||||
- **AND** both values MUST be formatted as traffic sizes based on MB values
|
||||
|
||||
#### Scenario: Show placeholder when usage summary is unavailable
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情或设备资产详情
|
||||
- **AND** `total_virtual_used_mb` or `total_virtual_remaining_mb` is `null` or absent
|
||||
- **WHEN** 页面渲染对应汇总字段
|
||||
- **THEN** 页面 MUST display `-` for the unavailable field
|
||||
- **AND** 页面 MUST NOT display `0` solely because the field is unavailable
|
||||
19
openspec/changes/update-asset-resolve-usage-summary/tasks.md
Normal file
19
openspec/changes/update-asset-resolve-usage-summary/tasks.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## 1. Contract
|
||||
|
||||
- [x] 1.1 扩展资产解析接口方法,支持传入 `include_usage_summary` Query 参数。
|
||||
- [x] 1.2 扩展资产解析响应类型,新增 `total_virtual_used_mb?: number | null` 和 `total_virtual_remaining_mb?: number | null`。
|
||||
- [x] 1.3 确保后台“资产信息”页调用 `GET /api/admin/assets/resolve/:identifier` 时默认传 `include_usage_summary=true`。
|
||||
|
||||
## 2. State And Display
|
||||
|
||||
- [x] 2.1 在资产信息页面状态中保留 `total_virtual_used_mb` 和 `total_virtual_remaining_mb`。
|
||||
- [x] 2.2 在卡资产“卡信息”区域的“运营商状态”后新增“所有套餐已用量”和“所有套餐剩余量”。
|
||||
- [x] 2.3 在设备资产“设备信息”区域的“SN”后新增“所有套餐已用量”和“所有套餐剩余量”。
|
||||
- [x] 2.4 新增字段按流量大小格式化展示;当响应字段为 `null`、`undefined` 或未请求时显示 `-`。
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 验证资产信息页请求解析接口时 URL 包含 `include_usage_summary=true`。
|
||||
- [ ] 3.2 验证解析接口返回两个汇总字段时,卡资产详情在“运营商状态”后正确展示。
|
||||
- [ ] 3.3 验证解析接口返回两个汇总字段时,设备资产详情在“SN”后正确展示。
|
||||
- [ ] 3.4 验证两个汇总字段为 `null` 或缺失时,页面显示 `-` 且不报错。
|
||||
@@ -0,0 +1,44 @@
|
||||
# Change: 更新资产信息账号类型字段可见性与运营商账户展示
|
||||
|
||||
## Why
|
||||
|
||||
资产信息页当前在卡/设备基础信息区域存在两类与最新业务口径不一致的问题:
|
||||
|
||||
- 卡资产“卡信息”中的 `实名认证策略` 仍会对 `user_type=3`(代理账号)和 `user_type=4`(企业账号)显示。
|
||||
- 设备资产“设备信息”中的 `实名认证策略` 和 `制造商` 仍会对 `user_type=3/4` 显示。
|
||||
- 卡资产“运营商账户”当前展示的是 `carrier_name`,而最新要求需要改为展示 `carrier_type`,并按运营商管理中的映射规则转成中文,例如 `CTCC -> 中国电信`。
|
||||
|
||||
如果不统一这些展示规则,不同账号在资产信息页会继续看到不应暴露的字段,同时“运营商账户”的显示口径也会与系统内其他模块不一致。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 更新卡资产“卡信息”区域:
|
||||
- 当当前登录账号 `user_type` 为 `3` 或 `4` 时,隐藏 `实名认证策略` 字段。
|
||||
- 当当前登录账号 `user_type` 为 `3` 或 `4` 时,`运营商账户` 字段改为展示 `carrier_type` 的中文映射值,而不是 `carrier_name`。
|
||||
- 当当前登录账号 `user_type` 不是 `3` 且不是 `4` 时,`运营商账户` 字段继续保持当前 `carrier_name` 展示口径。
|
||||
- 已知运营商类型映射沿用运营商管理口径:`CMCC -> 中国移动`、`CUCC -> 中国联通`、`CTCC -> 中国电信`、`CBN -> 中国广电`。
|
||||
- 当 `carrier_type` 为空时显示 `-`;若返回未知编码,则保留原编码展示,避免吞掉上游信息。
|
||||
- 更新设备资产“设备信息”区域:
|
||||
- 当当前登录账号 `user_type` 为 `3` 或 `4` 时,隐藏 `实名认证策略` 字段。
|
||||
- 当当前登录账号 `user_type` 为 `3` 或 `4` 时,隐藏 `制造商` 字段。
|
||||
- 范围约束:
|
||||
- 本提案仅调整资产信息基础描述区字段展示。
|
||||
- 本提案不改动实名认证策略相关操作按钮的权限和可见性。
|
||||
- 本提案不改动设备详情“绑定卡列表”中的列展示规则,除非后续另行提出变更。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `asset-information`
|
||||
- Affected code:
|
||||
- `src/views/asset-management/asset-information/components/BasicInfoCard.vue`
|
||||
- `src/views/asset-management/asset-information/composables/useAssetFormatters.ts`
|
||||
- `src/views/asset-management/asset-information/types.ts`
|
||||
- `src/views/asset-management/asset-information/composables/useAssetInfo.ts`
|
||||
- `src/config/constants/carrierTypes.ts`
|
||||
- Dependencies:
|
||||
- 依赖当前登录账号 `user_type` 可从用户状态中稳定读取
|
||||
- 依赖资产详情接口继续返回 `carrier_type`
|
||||
- 运营商类型中文映射应与运营商管理模块保持一致
|
||||
- Breaking changes:
|
||||
- 无外部接口破坏性变更;本次仅调整后台页面展示契约和字段可见性
|
||||
@@ -0,0 +1,123 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Admin Card Asset Carrier Type Contract
|
||||
|
||||
The admin asset information integration SHALL accept and preserve `carrier_type` from `GET /api/admin/assets/resolve/:identifier` for card assets so the card information section can render the carrier account value from the carrier type code.
|
||||
|
||||
#### Scenario: Card asset detail preserves carrier_type from resolve response
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** `GET /api/admin/assets/resolve/:identifier` 返回了 `carrier_type`
|
||||
- **WHEN** 前端解析卡资产详情响应
|
||||
- **THEN** 页面状态 MUST 保留该 `carrier_type` 原始值
|
||||
- **AND** 后续“卡信息”区域的 `运营商账户` 展示 MUST 基于该状态值进行格式化
|
||||
|
||||
#### Scenario: Missing carrier_type does not break card asset detail parsing
|
||||
|
||||
- **GIVEN** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** `GET /api/admin/assets/resolve/:identifier` 未返回 `carrier_type` 或该字段为空
|
||||
- **WHEN** 前端解析卡资产详情响应
|
||||
- **THEN** 页面 MUST 继续保留其他卡资产字段的解析行为
|
||||
- **AND** MUST NOT 因 `carrier_type` 缺失导致“卡信息”区域渲染失败
|
||||
|
||||
### Requirement: Admin Card Asset Carrier Account Display
|
||||
|
||||
The admin asset information view SHALL render the card information field `运营商账户` according to the current logged-in account type.
|
||||
|
||||
For logged-in accounts whose `user_type` is `3` (代理账号) or `4` (企业账号), the field MUST use `carrier_type` and the same carrier-type-to-Chinese mapping used by carrier management.
|
||||
|
||||
Known mappings MUST include `CMCC -> 中国移动`, `CUCC -> 中国联通`, `CTCC -> 中国电信`, and `CBN -> 中国广电`.
|
||||
|
||||
For other logged-in account types, the field MUST keep the existing `carrier_name` display behavior.
|
||||
|
||||
If `carrier_type` is empty or missing, the page MUST display `-`. If `carrier_type` contains an unknown code, the page MUST preserve and display the original code.
|
||||
|
||||
#### Scenario: Show China Telecom label for CTCC in card information for agent or enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `3` 或 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** 当前卡资产详情状态中的 `carrier_type` 为 `CTCC`
|
||||
- **WHEN** 页面渲染“卡信息”区域中的 `运营商账户`
|
||||
- **THEN** 页面 MUST 显示 `中国电信`
|
||||
|
||||
#### Scenario: Show placeholder when carrier_type is empty for agent or enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `3` 或 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** 当前卡资产详情状态中的 `carrier_type` 为空、缺失或仅包含空白字符
|
||||
- **WHEN** 页面渲染“卡信息”区域中的 `运营商账户`
|
||||
- **THEN** 页面 MUST 显示 `-`
|
||||
|
||||
#### Scenario: Preserve unknown carrier_type code for agent or enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `3` 或 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** 当前卡资产详情状态中的 `carrier_type` 为未收录的编码
|
||||
- **WHEN** 页面渲染“卡信息”区域中的 `运营商账户`
|
||||
- **THEN** 页面 MUST 直接显示该原始编码
|
||||
- **AND** MUST NOT 错误映射为其他运营商中文名称
|
||||
|
||||
#### Scenario: Keep carrier_name display for non-agent non-enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 不是 `3` 也不是 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **AND** 当前卡资产详情状态中的 `carrier_name` 为有效值
|
||||
- **WHEN** 页面渲染“卡信息”区域中的 `运营商账户`
|
||||
- **THEN** 页面 MUST 显示该 `carrier_name`
|
||||
|
||||
### Requirement: Admin Card Realname Policy Visibility By Account Type
|
||||
|
||||
The admin asset information view SHALL hide the card information field `实名认证策略` for logged-in accounts whose `user_type` is `3` (代理账号) or `4` (企业账号).
|
||||
|
||||
For other logged-in account types, the page SHALL keep the existing card information field visibility behavior for `实名认证策略`.
|
||||
|
||||
#### Scenario: Hide card realname policy for agent account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `3`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **WHEN** 页面渲染“卡信息”区域
|
||||
- **THEN** 页面 MUST NOT 显示 `实名认证策略` 字段
|
||||
|
||||
#### Scenario: Hide card realname policy for enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **WHEN** 页面渲染“卡信息”区域
|
||||
- **THEN** 页面 MUST NOT 显示 `实名认证策略` 字段
|
||||
|
||||
#### Scenario: Keep card realname policy visible for non-agent non-enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 不是 `3` 也不是 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的卡资产详情
|
||||
- **WHEN** 页面渲染“卡信息”区域
|
||||
- **THEN** 页面 MUST 继续按现有规则渲染 `实名认证策略` 字段
|
||||
|
||||
### Requirement: Admin Device Sensitive Field Visibility By Account Type
|
||||
|
||||
The admin asset information view SHALL hide the device information fields `实名认证策略` and `制造商` for logged-in accounts whose `user_type` is `3` (代理账号) or `4` (企业账号).
|
||||
|
||||
For other logged-in account types, the page SHALL keep the existing device information field visibility behavior for both fields.
|
||||
|
||||
#### Scenario: Hide device realname policy and manufacturer for agent account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `3`
|
||||
- **AND** 用户正在查看后台“资产信息”中的设备资产详情
|
||||
- **WHEN** 页面渲染“设备信息”区域
|
||||
- **THEN** 页面 MUST NOT 显示 `实名认证策略` 字段
|
||||
- **AND** 页面 MUST NOT 显示 `制造商` 字段
|
||||
|
||||
#### Scenario: Hide device realname policy and manufacturer for enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 是 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的设备资产详情
|
||||
- **WHEN** 页面渲染“设备信息”区域
|
||||
- **THEN** 页面 MUST NOT 显示 `实名认证策略` 字段
|
||||
- **AND** 页面 MUST NOT 显示 `制造商` 字段
|
||||
|
||||
#### Scenario: Keep device realname policy and manufacturer visible for non-agent non-enterprise account
|
||||
|
||||
- **GIVEN** 当前登录账号的 `user_type` 不是 `3` 也不是 `4`
|
||||
- **AND** 用户正在查看后台“资产信息”中的设备资产详情
|
||||
- **WHEN** 页面渲染“设备信息”区域
|
||||
- **THEN** 页面 MUST 继续按现有规则渲染 `实名认证策略` 字段
|
||||
- **AND** 页面 MUST 继续按现有规则渲染 `制造商` 字段
|
||||
@@ -0,0 +1,20 @@
|
||||
## 1. Contract And Formatting
|
||||
|
||||
- [x] 1.1 补齐资产信息页面本地类型/状态中的 `carrier_type` 字段,确保卡资产详情可稳定保留该值。
|
||||
- [x] 1.2 复用运营商管理中的运营商类型映射规则,为资产信息页提供 `carrier_type` 到中文文案的格式化能力。
|
||||
|
||||
## 2. UI Visibility
|
||||
|
||||
- [x] 2.1 调整卡资产“卡信息”区域的 `运营商账户` 字段:`user_type=3/4` 展示 `carrier_type` 中文映射,其他账号继续展示原来的 `carrier_name`。
|
||||
- [x] 2.2 当当前登录账号 `user_type` 为 `3` 或 `4` 时,隐藏卡资产“卡信息”区域的 `实名认证策略` 字段。
|
||||
- [x] 2.3 当当前登录账号 `user_type` 为 `3` 或 `4` 时,隐藏设备资产“设备信息”区域的 `实名认证策略` 和 `制造商` 字段。
|
||||
- [x] 2.4 确认本次变更仅影响基础信息描述区,不改动实名认证策略操作按钮和设备绑定卡列表列。
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 验证 `user_type=3/4` 查看卡资产且返回 `carrier_type=CTCC` 时,“运营商账户”显示为 `中国电信`。
|
||||
- [ ] 3.2 验证 `user_type=3/4` 查看卡资产且返回 `carrier_type=CMCC/CUCC/CBN` 时,“运营商账户”分别显示与运营商管理一致的中文文案;空值显示 `-`。
|
||||
- [ ] 3.3 验证 `user_type=3/4` 查看卡资产且返回未知 `carrier_type` 编码时,“运营商账户”保留原编码展示。
|
||||
- [ ] 3.4 验证 `user_type=3` 和 `user_type=4` 查看卡资产时,不显示 `实名认证策略` 字段。
|
||||
- [ ] 3.5 验证 `user_type=3` 和 `user_type=4` 查看设备资产时,不显示 `实名认证策略` 和 `制造商` 字段。
|
||||
- [ ] 3.6 验证其他账号类型查看卡资产时,“运营商账户”继续显示原来的 `carrier_name`;其余上述字段继续按现有规则显示。
|
||||
@@ -0,0 +1,39 @@
|
||||
# Change: 允许企业账号登录后台管理系统
|
||||
|
||||
## Why
|
||||
|
||||
当前前端登录流程会在登录接口成功返回后,基于 `user_type=4` 对企业账号做前端拦截,并提示“企业账号无法登录后台管理系统,请使用平台/代理账号登录”。这与最新业务要求不一致,导致企业账号即使后端已返回有效 token、菜单和权限,也无法进入后台。
|
||||
|
||||
如果只保留现状,企业账号无法访问后台中已经按权限和菜单控制开放给它们的页面,也会与近期多项针对企业账号后台可见性和资产能力的变更方向相冲突。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 移除前端登录流程中对 `user_type=4` 企业账号的后台登录拦截。
|
||||
- 企业账号登录成功后,前端应与其他账号类型一致地:
|
||||
- 保存 token
|
||||
- 保存用户信息
|
||||
- 保存权限、菜单和按钮数据
|
||||
- 在存在可用后台菜单和权限时,继续进入后台默认首页/重定向目标页
|
||||
- 当登录接口成功但 `menus` 为空或 `permissions` 为空时:
|
||||
- 前端仅提示“当前账号暂无任何权限,请联系管理员分配角色和权限”
|
||||
- 不继续进入后台首页或动态路由注册流程
|
||||
- 不作为这条提示的后续动作调用退出登录接口
|
||||
- 登录成功后的页面访问边界继续由现有菜单、路由权限、按钮权限和后端接口权限共同控制。
|
||||
- 本提案不要求本次同步放开所有企业账号受限接口或页面;仅调整“企业账号能否进入后台”的前端准入逻辑。
|
||||
- 本提案不修改登录接口路径、请求参数或返回结构。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `admin-authentication`
|
||||
- Affected code:
|
||||
- `src/composables/useLogin.ts`
|
||||
- `src/router/guards/beforeEach.ts`
|
||||
- `src/router/guards/permission.ts`
|
||||
- `src/App.vue`
|
||||
- 其他依赖当前登录态、菜单和权限进行页面准入的后台入口逻辑
|
||||
- Dependencies:
|
||||
- 后端登录接口继续允许企业账号返回有效 `access_token`、`user`、`permissions`、`menus`、`buttons`
|
||||
- 企业账号需要至少具备一部分后台菜单或权限,才能进入可访问页面
|
||||
- Breaking changes:
|
||||
- 无外部接口破坏性变更;本次为前端登录准入行为放宽
|
||||
@@ -0,0 +1,70 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Enterprise Account Admin Login Admission
|
||||
|
||||
The admin login flow SHALL allow enterprise accounts whose `user_type` is `4` to sign in to the admin system when the login API returns a successful response with valid authentication data.
|
||||
|
||||
The frontend MUST NOT reject admin login solely because `user_type` equals `4`.
|
||||
|
||||
#### Scenario: Enterprise account is allowed to complete admin login
|
||||
|
||||
- **GIVEN** 用户使用企业账号凭证登录后台管理系统
|
||||
- **AND** 登录接口返回成功状态
|
||||
- **AND** 返回数据包含有效 `access_token`
|
||||
- **AND** 返回数据中的 `user.user_type` 为 `4`
|
||||
- **WHEN** 前端处理登录成功响应
|
||||
- **THEN** 前端 MUST 继续执行后台登录成功流程
|
||||
- **AND** MUST NOT 弹出“企业账号无法登录后台管理系统,请使用平台/代理账号登录”提示
|
||||
|
||||
#### Scenario: Enterprise account login still fails when login API itself fails
|
||||
|
||||
- **GIVEN** 用户使用企业账号凭证登录后台管理系统
|
||||
- **AND** 登录接口返回失败状态,或未返回有效 `access_token`
|
||||
- **WHEN** 前端处理登录响应
|
||||
- **THEN** 前端 MUST 继续按现有登录失败流程处理
|
||||
- **AND** MUST NOT 将“允许企业账号登录后台”解释为绕过登录接口失败
|
||||
|
||||
### Requirement: Enterprise Account Login State Persistence
|
||||
|
||||
The admin login flow SHALL persist enterprise-account login state using the same token, user, permissions, menus, and buttons handling used for other successful admin logins only when the login response provides usable admin-access data.
|
||||
|
||||
#### Scenario: Enterprise account saves auth state after successful login with usable admin access data
|
||||
|
||||
- **GIVEN** 用户使用企业账号成功登录后台管理系统
|
||||
- **AND** 登录返回中包含 `access_token`、`refresh_token`、`user`、`permissions`、`menus`、`buttons`
|
||||
- **AND** `permissions` 非空
|
||||
- **AND** `menus` 非空
|
||||
- **WHEN** 前端写入登录态
|
||||
- **THEN** 前端 MUST 保存 token
|
||||
- **AND** MUST 保存用户信息
|
||||
- **AND** MUST 保存权限、菜单和按钮数据
|
||||
|
||||
#### Scenario: Enterprise account shows no-permission warning without logout follow-up
|
||||
|
||||
- **GIVEN** 用户使用企业账号成功登录后台管理系统
|
||||
- **AND** 登录返回中的 `permissions` 为空,或 `menus` 为空
|
||||
- **WHEN** 前端处理登录成功响应
|
||||
- **THEN** 前端 MUST 继续显示现有“当前账号暂无任何权限,请联系管理员分配角色和权限”提示
|
||||
- **AND** MUST NOT 因为空权限或空菜单回退为企业账号登录拦截
|
||||
- **AND** MUST NOT 继续进入后台首页或动态路由注册流程
|
||||
- **AND** MUST NOT 因这条无权限提示而调用退出登录接口
|
||||
|
||||
### Requirement: Enterprise Account Admin Entry Uses Existing Authorization Boundaries
|
||||
|
||||
Allowing enterprise accounts to sign in to the admin system SHALL NOT by itself grant additional page or API access beyond the existing menu, route-permission, button-permission, and backend authorization boundaries.
|
||||
|
||||
#### Scenario: Enterprise account enters admin and follows existing route registration flow
|
||||
|
||||
- **GIVEN** 用户使用企业账号成功登录后台管理系统
|
||||
- **AND** 登录返回中的 `permissions` 非空
|
||||
- **AND** 登录返回中的 `menus` 非空
|
||||
- **WHEN** 前端进入登录后的路由注册和首页跳转流程
|
||||
- **THEN** 前端 MUST 继续按现有菜单和动态路由注册逻辑处理
|
||||
- **AND** MUST NOT 因 `user_type=4` 在该阶段额外拒绝用户进入后台
|
||||
|
||||
#### Scenario: Enterprise account page access remains permission-controlled
|
||||
|
||||
- **GIVEN** 用户已使用企业账号登录后台管理系统
|
||||
- **WHEN** 用户尝试访问某个后台页面或触发某个后台操作
|
||||
- **THEN** 页面访问和操作可用性 MUST 继续由现有菜单、路由权限、按钮权限和后端接口鉴权决定
|
||||
- **AND** 允许登录后台本身 MUST NOT 被解释为默认拥有全部后台页面和操作权限
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. Login Flow
|
||||
|
||||
- [x] 1.1 移除登录成功后对 `user_type=4` 企业账号的前端拒绝逻辑和错误提示。
|
||||
- [x] 1.2 确保企业账号在登录成功后与其他账号一样完成 token、用户信息、权限、菜单、按钮的保存流程。
|
||||
- [x] 1.3 当登录成功但 `menus` 或 `permissions` 为空时,仅提示“当前账号暂无任何权限,请联系管理员分配角色和权限”并停止后续登录流程,不调用退出登录接口。
|
||||
|
||||
## 2. Navigation And Access
|
||||
|
||||
- [x] 2.1 确保企业账号在具备可用后台菜单和权限时,继续走现有后台首页跳转和路由注册流程,不因账号类型被额外拦截。
|
||||
- [x] 2.2 确认企业账号后台访问范围仍由现有菜单、路由权限和后端接口权限控制,而不是在登录阶段一刀切拒绝。
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [ ] 3.1 验证企业账号登录成功后不再出现“企业账号无法登录后台管理系统,请使用平台/代理账号登录”提示。
|
||||
- [ ] 3.2 验证企业账号在具备可用后台菜单和权限时,能正常保存登录态并进入后台首页或重定向目标页。
|
||||
- [ ] 3.3 验证企业账号在菜单为空或权限为空时,仅提示“暂无任何权限”,且不会继续调用退出登录接口。
|
||||
- [ ] 3.4 验证平台账号和代理账号登录流程不受影响。
|
||||
67
openspec/changes/update-export-task-scene-pages/design.md
Normal file
67
openspec/changes/update-export-task-scene-pages/design.md
Normal file
@@ -0,0 +1,67 @@
|
||||
## Context
|
||||
|
||||
导出任务能力已经具备统一 API、通用列表页面、详情页面和创建弹窗。现有列表页面通过 `scene` 搜索项区分设备、IOT 卡和订单任务,但新的产品方案要求用三个固定场景页面替代该筛选方式,并让三个页面按钮权限相互独立。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
- Goals: 在导出管理下提供“导出设备”“导出IOT卡”“导出订单”三个页面。
|
||||
- Goals: 三个页面固定传入各自 `scene`,去掉页面中的“导出场景”筛选。
|
||||
- Goals: 三个页面及其按钮/操作使用场景级独立权限编码。
|
||||
- Goals: 尽量复用现有导出任务列表和详情实现,避免复制三份完整页面逻辑。
|
||||
- Non-Goals: 不新增导出任务后端 API。
|
||||
- Non-Goals: 不改变创建导出任务的 `scene` 值和 `query` 快照语义。
|
||||
- Non-Goals: 不调整导出任务状态流转、文件生成或下载地址生成逻辑。
|
||||
|
||||
## Decisions
|
||||
|
||||
- Decision: 使用一个可复用导出任务列表实现,通过路由配置或组件 props 注入固定场景、页面标题和权限映射。
|
||||
- Rationale: 三个页面除固定 `scene` 和权限外,分页、状态筛选、时间筛选、下载、取消逻辑一致,复用可以减少重复代码和后续维护成本。
|
||||
|
||||
- Decision: 页面查询参数由页面固定注入 `scene`,不从搜索表单读取“导出场景”。
|
||||
- Rationale: 页面已经按场景拆分,继续暴露场景筛选会造成入口语义冲突,也可能让用户在“导出设备”页面查到 IOT 卡或订单任务。
|
||||
|
||||
- Decision: 权限编码按场景和操作拆分,默认建议如下。
|
||||
- Rationale: 用户明确要求三个界面的按钮权限编码不同;列表访问、详情、下载、取消均需要避免跨场景复用。
|
||||
|
||||
| 场景 | 页面访问 | 详情 | 下载 | 取消 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 导出设备 | `export_task:device:list` | `export_task:device:detail` | `export_task:device:download` | `export_task:device:cancel` |
|
||||
| 导出IOT卡 | `export_task:iot_card:list` | `export_task:iot_card:detail` | `export_task:iot_card:download` | `export_task:iot_card:cancel` |
|
||||
| 导出订单 | `export_task:order:list` | `export_task:order:detail` | `export_task:order:download` | `export_task:order:cancel` |
|
||||
|
||||
- Decision: 业务列表创建导出任务按钮继续使用来源业务权限,默认建议如下。
|
||||
- Rationale: 创建导出任务属于来源业务列表能力,不应因为拥有某个导出任务管理页面权限就获得业务列表导出入口。
|
||||
|
||||
| 入口 | 创建导出任务权限 | scene |
|
||||
| --- | --- | --- |
|
||||
| 设备管理 | `devices:export` | `device` |
|
||||
| IOT 卡管理 | `iot_card:export` | `iot_card` |
|
||||
| 订单列表 | `orders:export` | `order` |
|
||||
|
||||
- Decision: 详情页可继续复用同一组件,但必须根据任务场景或来源页面使用对应场景的详情、下载、取消权限。
|
||||
- Rationale: 详情页面本身也包含下载和取消按钮,若继续使用通用权限会绕过三个列表页面的按钮隔离要求。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Risk: 后端或权限配置已经发放了通用 `export_task:*` 权限,切换为场景级权限后用户可能暂时看不到页面或按钮。
|
||||
- Mitigation: 实施时同步更新权限种子/菜单配置,并在上线前完成角色权限迁移或重新授权。
|
||||
|
||||
- Risk: 订单导出场景提案正在独立推进,可能继续给旧“导出列表”增加订单筛选。
|
||||
- Mitigation: 实施本变更前先合并范围,保留 `scene=order` 和订单创建入口,移除旧列表中的订单场景筛选实现。
|
||||
|
||||
- Risk: 详情页直接通过 URL 打开时缺少来源页面上下文。
|
||||
- Mitigation: 详情页读取任务详情后按返回的 `scene` 计算对应权限,缺少该场景详情权限时展示无权限提示并阻止后续按钮操作。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 增加三条导出任务列表路由和菜单项,删除或隐藏旧通用“导出列表”入口。
|
||||
2. 将导出任务列表改造成固定场景列表,移除“导出场景”搜索项和冗余场景列。
|
||||
3. 为三页分别注入固定 `scene`、标题和场景级权限映射。
|
||||
4. 调整导出任务详情页和行操作的权限判断,改用任务场景对应权限。
|
||||
5. 同步订单导出入口,确保订单导出任务进入“导出订单”页面查看。
|
||||
6. 更新后端/菜单权限配置并完成角色授权迁移。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 最终权限编码是否采用本设计建议,还是需要对齐后端已有权限命名规范。
|
||||
- 旧通用导出列表路由是否直接移除,还是短期隐藏并重定向到用户有权限的第一个场景页面。
|
||||
39
openspec/changes/update-export-task-scene-pages/proposal.md
Normal file
39
openspec/changes/update-export-task-scene-pages/proposal.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Change: 拆分导出任务列表为固定场景页面
|
||||
|
||||
## Why
|
||||
|
||||
当前导出管理只有一个“导出列表”页面,需要用户通过“导出场景”筛选设备、IOT 卡和订单导出任务。产品希望把该页面拆成三个固定场景页面,入口更直接,并且三个页面及页面按钮使用不同权限编码。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 将导出管理下的单个“导出列表”替换为三个页面:
|
||||
- 导出设备
|
||||
- 导出IOT卡
|
||||
- 导出订单
|
||||
- 三个页面分别固定查询 `scene=device`、`scene=iot_card`、`scene=order`,页面内不再展示或提交用户可选的“导出场景”筛选项。
|
||||
- 导出列表中的“场景”列可移除或默认不展示,因为页面本身已经代表固定场景。
|
||||
- 三个页面的菜单访问、详情、下载、取消等按钮/操作均使用场景级独立权限编码,不再复用同一组通用导出任务按钮权限。
|
||||
- 业务列表发起导出的按钮权限继续按来源业务隔离:设备导出、IOT 卡导出、订单导出使用不同权限编码。
|
||||
- 订单导出任务仍使用 `scene=order`,并与现有订单导出场景提案合并实施或调整其导出列表筛选相关内容。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `export-task-management`
|
||||
- Affected code:
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/views/asset-management/export-task-management/export-task-list/index.vue`
|
||||
- `src/views/asset-management/export-task-management/export-task-detail/index.vue`
|
||||
- `src/types/api/exportTask.ts`
|
||||
- `src/components/business/ExportTaskCreateDialog.vue`
|
||||
- `src/views/asset-management/device-list/index.vue`
|
||||
- `src/views/asset-management/iot-card-management/index.vue`
|
||||
- `src/views/order-management/order-list/index.vue`
|
||||
- 菜单、权限和国际化文案配置
|
||||
- Dependencies:
|
||||
- 后端导出任务列表接口继续支持 `scene=device`、`scene=iot_card`、`scene=order` 过滤。
|
||||
- 后端/权限配置需要提供三套场景级导出任务权限编码。
|
||||
- 现有 `add-order-export-scene` 提案与本提案存在重叠:其“在导出列表场景筛选中新增订单”应被本提案的三个固定页面方案替代。
|
||||
- Breaking changes:
|
||||
- 原通用“导出列表”菜单入口和 `export_task:list/detail/download/cancel` 这组通用页面操作权限将被场景级页面/按钮权限取代。
|
||||
@@ -0,0 +1,111 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Scene-Specific Export Task Pages
|
||||
|
||||
The admin frontend SHALL replace the single generic export task list entry with three scene-specific export task pages: 导出设备, 导出IOT卡, and 导出订单.
|
||||
|
||||
#### Scenario: Render scene-specific export task navigation
|
||||
|
||||
- **GIVEN** 用户打开资产管理下的导出管理菜单
|
||||
- **WHEN** 系统渲染导出任务导航
|
||||
- **THEN** 系统 MUST provide separate entries for 导出设备, 导出IOT卡, and 导出订单
|
||||
- **AND** 系统 MUST NOT expose the old generic 导出列表 menu entry as the primary navigation entry
|
||||
|
||||
#### Scenario: Query device export tasks
|
||||
|
||||
- **GIVEN** 用户打开导出设备页面
|
||||
- **WHEN** 页面查询导出任务列表
|
||||
- **THEN** 系统 MUST call `GET /api/admin/export-tasks` with `scene=device`
|
||||
- **AND** 用户 MUST NOT be able to change this page to query `iot_card` or `order` tasks through a scene selector
|
||||
|
||||
#### Scenario: Query IOT card export tasks
|
||||
|
||||
- **GIVEN** 用户打开导出IOT卡页面
|
||||
- **WHEN** 页面查询导出任务列表
|
||||
- **THEN** 系统 MUST call `GET /api/admin/export-tasks` with `scene=iot_card`
|
||||
- **AND** 用户 MUST NOT be able to change this page to query `device` or `order` tasks through a scene selector
|
||||
|
||||
#### Scenario: Query order export tasks
|
||||
|
||||
- **GIVEN** 用户打开导出订单页面
|
||||
- **WHEN** 页面查询导出任务列表
|
||||
- **THEN** 系统 MUST call `GET /api/admin/export-tasks` with `scene=order`
|
||||
- **AND** 用户 MUST NOT be able to change this page to query `device` or `iot_card` tasks through a scene selector
|
||||
|
||||
### Requirement: Remove Export Scene Filtering From Scene Pages
|
||||
|
||||
Scene-specific export task pages SHALL remove user-facing export scene filtering because each page has exactly one fixed scene.
|
||||
|
||||
#### Scenario: Hide scene search field
|
||||
|
||||
- **GIVEN** 用户打开任一导出任务场景页面
|
||||
- **WHEN** 页面渲染搜索表单
|
||||
- **THEN** 页面 MUST NOT display a 导出场景 search item
|
||||
- **AND** reset and search actions MUST keep the page's fixed `scene` unchanged
|
||||
|
||||
#### Scenario: Avoid redundant scene column
|
||||
|
||||
- **GIVEN** 用户打开任一导出任务场景页面
|
||||
- **WHEN** 页面渲染任务表格
|
||||
- **THEN** 页面 SHOULD remove or hide the 场景 column by default
|
||||
- **AND** 页面 MUST continue to display task number, status, progress, format, row counts, shard progress, error message, and created time
|
||||
|
||||
### Requirement: Scene-Specific Export Task Permissions
|
||||
|
||||
The admin frontend MUST gate each export task page and page action with scene-specific permission codes so that device, IOT card, and order export task permissions are independent.
|
||||
|
||||
#### Scenario: Gate scene pages independently
|
||||
|
||||
- **GIVEN** 用户 only has `export_task:device:list`
|
||||
- **WHEN** 系统渲染导出管理菜单
|
||||
- **THEN** 系统 MUST show 导出设备
|
||||
- **AND** 系统 MUST NOT show 导出IOT卡 or 导出订单 unless the user also has `export_task:iot_card:list` or `export_task:order:list`
|
||||
|
||||
#### Scenario: Gate device export task actions independently
|
||||
|
||||
- **GIVEN** 用户打开导出设备页面
|
||||
- **WHEN** 页面渲染详情、下载 or 取消操作
|
||||
- **THEN** 系统 MUST use `export_task:device:detail`, `export_task:device:download`, and `export_task:device:cancel` respectively
|
||||
- **AND** 系统 MUST NOT treat IOT card or order export task action permissions as sufficient for device actions
|
||||
|
||||
#### Scenario: Gate IOT card export task actions independently
|
||||
|
||||
- **GIVEN** 用户打开导出IOT卡页面
|
||||
- **WHEN** 页面渲染详情、下载 or 取消操作
|
||||
- **THEN** 系统 MUST use `export_task:iot_card:detail`, `export_task:iot_card:download`, and `export_task:iot_card:cancel` respectively
|
||||
- **AND** 系统 MUST NOT treat device or order export task action permissions as sufficient for IOT card actions
|
||||
|
||||
#### Scenario: Gate order export task actions independently
|
||||
|
||||
- **GIVEN** 用户打开导出订单页面
|
||||
- **WHEN** 页面渲染详情、下载 or 取消操作
|
||||
- **THEN** 系统 MUST use `export_task:order:detail`, `export_task:order:download`, and `export_task:order:cancel` respectively
|
||||
- **AND** 系统 MUST NOT treat device or IOT card export task action permissions as sufficient for order actions
|
||||
|
||||
#### Scenario: Keep business export buttons independent
|
||||
|
||||
- **GIVEN** 用户分别访问设备管理、IOT 卡管理 and 订单列表
|
||||
- **WHEN** 页面渲染创建导出任务按钮
|
||||
- **THEN** 设备管理 MUST use `devices:export`
|
||||
- **AND** IOT 卡管理 MUST use `iot_card:export`
|
||||
- **AND** 订单列表 MUST use `orders:export`
|
||||
- **AND** possessing one of these permissions MUST NOT reveal or enable the other two export buttons
|
||||
|
||||
### Requirement: Scene-Aware Export Task Detail Permissions
|
||||
|
||||
The export task detail page SHALL apply detail, download, and cancel permissions according to the task's scene.
|
||||
|
||||
#### Scenario: Enforce scene permissions on direct detail access
|
||||
|
||||
- **GIVEN** 用户直接打开某个导出任务详情 URL
|
||||
- **WHEN** 系统加载任务详情 and reads the returned `scene`
|
||||
- **THEN** 系统 MUST require the matching scene detail permission before showing the detail content
|
||||
- **AND** 系统 MUST use the matching scene download and cancel permissions before showing detail-page buttons
|
||||
|
||||
#### Scenario: Prevent cross-scene action leakage
|
||||
|
||||
- **GIVEN** 用户 has only order export task download permission
|
||||
- **AND** 用户打开 a completed device export task detail
|
||||
- **WHEN** 页面渲染详情操作区
|
||||
- **THEN** 页面 MUST NOT show the device task download button
|
||||
- **AND** 页面 MUST NOT allow downloading that device task through the order download permission
|
||||
46
openspec/changes/update-export-task-scene-pages/tasks.md
Normal file
46
openspec/changes/update-export-task-scene-pages/tasks.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## 1. Proposal Review
|
||||
|
||||
- [x] 1.1 确认三个页面名称为“导出设备”“导出IOT卡”“导出订单”。
|
||||
- [x] 1.2 确认固定场景值分别为 `device`、`iot_card`、`order`。
|
||||
- [x] 1.3 确认三套页面和按钮权限编码,或按后端权限命名规范替换设计中的默认建议。
|
||||
- [x] 1.4 确认本提案与 `add-order-export-scene` 合并实施,订单场景不再通过旧导出场景筛选进入。
|
||||
|
||||
## 2. Routing And Menu
|
||||
|
||||
- [x] 2.1 新增三条导出任务列表路由别名和路由记录。
|
||||
- [x] 2.2 在导出管理菜单下新增“导出设备”“导出IOT卡”“导出订单”。
|
||||
- [x] 2.3 删除或隐藏旧“导出列表”菜单入口。
|
||||
- [x] 2.4 为三条路由分别配置独立页面访问权限。
|
||||
- [x] 2.5 更新菜单国际化文案和权限配置。
|
||||
|
||||
## 3. Export Task List Pages
|
||||
|
||||
- [x] 3.1 将现有导出任务列表改造成可复用固定场景列表,支持传入页面标题、固定 `scene` 和权限映射。
|
||||
- [x] 3.2 移除搜索表单中的“导出场景”筛选项。
|
||||
- [x] 3.3 列表查询始终携带当前页面固定 `scene`。
|
||||
- [x] 3.4 移除或默认隐藏列表中的“场景”列。
|
||||
- [x] 3.5 保留任务状态、创建时间、分页、刷新等现有列表能力。
|
||||
|
||||
## 4. Scene-Specific Actions
|
||||
|
||||
- [x] 4.1 详情入口按当前页面场景检查对应 `detail` 权限。
|
||||
- [x] 4.2 下载按钮按当前页面场景检查对应 `download` 权限。
|
||||
- [x] 4.3 取消按钮按当前页面场景检查对应 `cancel` 权限。
|
||||
- [x] 4.4 导出任务详情页根据任务 `scene` 使用对应详情、下载、取消权限。
|
||||
- [x] 4.5 验证只有某一场景权限时,不会看到其他场景页面或按钮。
|
||||
|
||||
## 5. Business Export Entrypoints
|
||||
|
||||
- [x] 5.1 设备管理导出按钮继续使用 `devices:export` 并创建 `scene=device` 任务。
|
||||
- [x] 5.2 IOT 卡管理导出按钮继续使用 `iot_card:export` 并创建 `scene=iot_card` 任务。
|
||||
- [x] 5.3 订单列表导出按钮使用 `orders:export` 并创建 `scene=order` 任务。
|
||||
- [x] 5.4 确认三个业务导出按钮权限编码互不复用。
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- [x] 6.1 验证“导出设备”页面请求 `GET /api/admin/export-tasks` 时固定携带 `scene=device`。
|
||||
- [x] 6.2 验证“导出IOT卡”页面请求固定携带 `scene=iot_card`。
|
||||
- [x] 6.3 验证“导出订单”页面请求固定携带 `scene=order`。
|
||||
- [x] 6.4 验证三个页面均不展示“导出场景”筛选项。
|
||||
- [x] 6.5 验证三页详情、下载、取消按钮按各自权限显示和拦截。
|
||||
- [x] 6.6 运行类型检查、lint 和相关页面手动回归。
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/utils/business/apiRateLimit'
|
||||
import type {
|
||||
BaseResponse,
|
||||
AssetResolveParams,
|
||||
AssetResolveResponse,
|
||||
AssetRealtimeStatusResponse,
|
||||
AssetRefreshResponse,
|
||||
@@ -49,9 +50,13 @@ export class AssetService extends BaseService {
|
||||
* 支持虚拟号、ICCID、IMEI、SN、MSISDN
|
||||
* GET /api/admin/assets/resolve/:identifier
|
||||
* @param identifier 资产标识符(虚拟号、ICCID、IMEI、SN、MSISDN)
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static resolveAsset(identifier: string): Promise<BaseResponse<AssetResolveResponse>> {
|
||||
return this.getOne<AssetResolveResponse>(`/api/admin/assets/resolve/${identifier}`)
|
||||
static resolveAsset(
|
||||
identifier: string,
|
||||
params?: AssetResolveParams
|
||||
): Promise<BaseResponse<AssetResolveResponse>> {
|
||||
return this.getOne<AssetResolveResponse>(`/api/admin/assets/resolve/${identifier}`, params)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export { ShopSeriesGrantService } from './shopSeriesGrant'
|
||||
export { OrderService } from './order'
|
||||
export { AssetService } from './asset'
|
||||
export { AgentRechargeService } from './agentRecharge'
|
||||
export { WechatConfigService } from './wechatConfig'
|
||||
export { PaymentSettingsService } from './paymentSettings'
|
||||
export { ExchangeService } from './exchange'
|
||||
export { RefundService } from './refund'
|
||||
export { DataCleanupService } from './dataCleanup'
|
||||
|
||||
80
src/api/modules/paymentSettings.ts
Normal file
80
src/api/modules/paymentSettings.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 支付配置管理 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
PaymentSettings,
|
||||
PaymentSettingsQueryParams,
|
||||
PaymentSettingsListResponse,
|
||||
CreatePaymentSettingsRequest,
|
||||
UpdatePaymentSettingsRequest
|
||||
} from '@/types/api/paymentSettings'
|
||||
|
||||
const PAYMENT_SETTINGS_BASE_URL = '/api/admin/wechat-configs'
|
||||
|
||||
export class PaymentSettingsService extends BaseService {
|
||||
/**
|
||||
* 获取支付配置列表
|
||||
*/
|
||||
static getPaymentSettings(
|
||||
params?: PaymentSettingsQueryParams
|
||||
): Promise<BaseResponse<PaymentSettingsListResponse>> {
|
||||
return this.get<BaseResponse<PaymentSettingsListResponse>>(PAYMENT_SETTINGS_BASE_URL, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付配置详情
|
||||
*/
|
||||
static getPaymentSettingsById(id: number): Promise<BaseResponse<PaymentSettings>> {
|
||||
return this.get<BaseResponse<PaymentSettings>>(`${PAYMENT_SETTINGS_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付配置
|
||||
*/
|
||||
static createPaymentSettings(
|
||||
data: CreatePaymentSettingsRequest
|
||||
): Promise<BaseResponse<PaymentSettings>> {
|
||||
return this.post<BaseResponse<PaymentSettings>>(PAYMENT_SETTINGS_BASE_URL, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付配置
|
||||
*/
|
||||
static updatePaymentSettings(
|
||||
id: number,
|
||||
data: UpdatePaymentSettingsRequest
|
||||
): Promise<BaseResponse<PaymentSettings>> {
|
||||
return this.put<BaseResponse<PaymentSettings>>(`${PAYMENT_SETTINGS_BASE_URL}/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付配置
|
||||
*/
|
||||
static deletePaymentSettings(id: number): Promise<BaseResponse<void>> {
|
||||
return this.delete<BaseResponse<void>>(`${PAYMENT_SETTINGS_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活支付配置
|
||||
*/
|
||||
static activatePaymentSettings(id: number): Promise<BaseResponse<PaymentSettings>> {
|
||||
return this.post<BaseResponse<PaymentSettings>>(`${PAYMENT_SETTINGS_BASE_URL}/${id}/activate`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用支付配置
|
||||
*/
|
||||
static deactivatePaymentSettings(id: number): Promise<BaseResponse<PaymentSettings>> {
|
||||
return this.post<BaseResponse<PaymentSettings>>(`${PAYMENT_SETTINGS_BASE_URL}/${id}/deactivate`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生效的支付配置
|
||||
*/
|
||||
static getActivePaymentSettings(): Promise<BaseResponse<PaymentSettings>> {
|
||||
return this.get<BaseResponse<PaymentSettings>>(`${PAYMENT_SETTINGS_BASE_URL}/active`)
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* 支付配置管理 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
WechatConfig,
|
||||
WechatConfigQueryParams,
|
||||
WechatConfigListResponse,
|
||||
CreateWechatConfigRequest,
|
||||
UpdateWechatConfigRequest
|
||||
} from '@/types/api/wechatConfig'
|
||||
|
||||
const PAYMENT_CONFIG_BASE_URL = '/api/admin/wechat-configs'
|
||||
|
||||
export class WechatConfigService extends BaseService {
|
||||
/**
|
||||
* 获取支付配置列表
|
||||
*/
|
||||
static getWechatConfigs(
|
||||
params?: WechatConfigQueryParams
|
||||
): Promise<BaseResponse<WechatConfigListResponse>> {
|
||||
return this.get<BaseResponse<WechatConfigListResponse>>(PAYMENT_CONFIG_BASE_URL, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付配置详情
|
||||
*/
|
||||
static getWechatConfigById(id: number): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.get<BaseResponse<WechatConfig>>(`${PAYMENT_CONFIG_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付配置
|
||||
*/
|
||||
static createWechatConfig(data: CreateWechatConfigRequest): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.post<BaseResponse<WechatConfig>>(PAYMENT_CONFIG_BASE_URL, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付配置
|
||||
*/
|
||||
static updateWechatConfig(
|
||||
id: number,
|
||||
data: UpdateWechatConfigRequest
|
||||
): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.put<BaseResponse<WechatConfig>>(`${PAYMENT_CONFIG_BASE_URL}/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付配置
|
||||
*/
|
||||
static deleteWechatConfig(id: number): Promise<BaseResponse<void>> {
|
||||
return this.delete<BaseResponse<void>>(`${PAYMENT_CONFIG_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活支付配置
|
||||
*/
|
||||
static activateWechatConfig(id: number): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.post<BaseResponse<WechatConfig>>(`${PAYMENT_CONFIG_BASE_URL}/${id}/activate`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用支付配置
|
||||
*/
|
||||
static deactivateWechatConfig(id: number): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.post<BaseResponse<WechatConfig>>(`${PAYMENT_CONFIG_BASE_URL}/${id}/deactivate`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生效的支付配置
|
||||
*/
|
||||
static getActiveWechatConfig(): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.get<BaseResponse<WechatConfig>>(`${PAYMENT_CONFIG_BASE_URL}/active`)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ExportTaskService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { getExportTaskSceneName } from '@/config/constants'
|
||||
import type { CreateExportTaskResponse, ExportTaskFormat, ExportTaskScene } from '@/types/api'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -73,15 +74,7 @@
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const sceneName = computed(() => {
|
||||
const sceneMap: Record<ExportTaskScene, string> = {
|
||||
device: '设备管理',
|
||||
iot_card: 'IoT卡管理',
|
||||
order: '订单管理'
|
||||
}
|
||||
|
||||
return sceneMap[props.scene]
|
||||
})
|
||||
const sceneName = computed(() => getExportTaskSceneName(props.scene))
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
*/
|
||||
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElNotification, ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { HOME_PAGE, RoutesAlias } from '@/router/routesAlias'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import AppConfig from '@/config'
|
||||
import { AuthService } from '@/api/modules'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { saveCredentials, getRememberedCredentials } from '@/utils/auth/rememberPassword'
|
||||
import { usernameRules, passwordRules } from '@/utils/auth/loginValidation'
|
||||
import { getRedirectPath } from '@/router/guards/permission'
|
||||
|
||||
/**
|
||||
* 登录表单数据
|
||||
@@ -28,7 +27,6 @@ interface LoginForm {
|
||||
export function useLogin() {
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 表单引用
|
||||
@@ -108,9 +106,24 @@ export function useLogin() {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户类型 - 禁止企业账号登录
|
||||
if (response.data.user && response.data.user.user_type === 4) {
|
||||
ElMessage.error('企业账号无法登录后台管理系统,请使用平台/代理账号登录')
|
||||
if (!response.data.user) {
|
||||
ElMessage.error(t('login.error.getUserInfoFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功但没有后台菜单或权限时,仅提示并停止后续流程。
|
||||
// 这里不保存登录态,也不触发后续跳转,避免进入路由守卫后再连续调用退出接口。
|
||||
const hasPermissions =
|
||||
Array.isArray(response.data.permissions) && response.data.permissions.length > 0
|
||||
const hasMenus = Array.isArray(response.data.menus) && response.data.menus.length > 0
|
||||
|
||||
if (!hasPermissions || !hasMenus) {
|
||||
ElMessage.warning({
|
||||
message: '当前账号暂无任何权限,请联系管理员分配角色和权限',
|
||||
duration: 4000,
|
||||
showClose: true
|
||||
})
|
||||
saveCredentials(formData.username, formData.password, formData.rememberPassword)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,13 +131,8 @@ export function useLogin() {
|
||||
userStore.setToken(response.data.access_token, response.data.refresh_token)
|
||||
|
||||
// 保存用户信息
|
||||
if (response.data.user) {
|
||||
userStore.setUserInfo(response.data.user)
|
||||
userStore.setLoginStatus(true)
|
||||
} else {
|
||||
ElMessage.error(t('login.error.getUserInfoFailed'))
|
||||
return
|
||||
}
|
||||
userStore.setUserInfo(response.data.user)
|
||||
userStore.setLoginStatus(true)
|
||||
|
||||
// 保存权限、菜单和按钮
|
||||
if (response.data.permissions) {
|
||||
@@ -137,19 +145,6 @@ export function useLogin() {
|
||||
userStore.setButtons(response.data.buttons)
|
||||
}
|
||||
|
||||
// 检查权限和菜单是否为空
|
||||
const hasPermissions = response.data.permissions && response.data.permissions.length > 0
|
||||
const hasMenus = response.data.menus && response.data.menus.length > 0
|
||||
|
||||
if (!hasPermissions || !hasMenus) {
|
||||
ElMessage.warning({
|
||||
message: '当前账号暂无任何权限,请联系管理员分配角色和权限',
|
||||
duration: 4000,
|
||||
showClose: true
|
||||
})
|
||||
// 虽然没有权限,但仍然允许登录,让用户看到空白页面而不是停留在登录页
|
||||
}
|
||||
|
||||
// 保存记住密码
|
||||
saveCredentials(formData.username, formData.password, formData.rememberPassword)
|
||||
|
||||
|
||||
55
src/config/constants/exportTask.ts
Normal file
55
src/config/constants/exportTask.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { ExportTaskScene } from '@/types/api'
|
||||
|
||||
export interface ExportTaskScenePermissions {
|
||||
detail: string
|
||||
download: string
|
||||
cancel: string
|
||||
}
|
||||
|
||||
export interface ExportTaskSceneConfig {
|
||||
scene: ExportTaskScene
|
||||
sceneName: string
|
||||
pageTitle: string
|
||||
permissions: ExportTaskScenePermissions
|
||||
}
|
||||
|
||||
export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneConfig> = {
|
||||
device: {
|
||||
scene: 'device',
|
||||
sceneName: '设备管理',
|
||||
pageTitle: '导出设备',
|
||||
permissions: {
|
||||
detail: 'export_task:device_detail',
|
||||
download: 'export_task:device_download',
|
||||
cancel: 'export_task:device_cancel'
|
||||
}
|
||||
},
|
||||
iot_card: {
|
||||
scene: 'iot_card',
|
||||
sceneName: 'IOT卡管理',
|
||||
pageTitle: '导出IOT卡',
|
||||
permissions: {
|
||||
detail: 'export_task:iot_card_detail',
|
||||
download: 'export_task:iot_card_download',
|
||||
cancel: 'export_task:iot_card_cancel'
|
||||
}
|
||||
},
|
||||
order: {
|
||||
scene: 'order',
|
||||
sceneName: '订单管理',
|
||||
pageTitle: '导出订单',
|
||||
permissions: {
|
||||
detail: 'export_task:order_detail',
|
||||
download: 'export_task:order_download',
|
||||
cancel: 'export_task:order_cancel'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getExportTaskSceneConfig = (scene?: ExportTaskScene) => {
|
||||
return scene ? EXPORT_TASK_SCENE_CONFIG[scene] : undefined
|
||||
}
|
||||
|
||||
export const getExportTaskSceneName = (scene?: ExportTaskScene) => {
|
||||
return getExportTaskSceneConfig(scene)?.sceneName || '-'
|
||||
}
|
||||
@@ -31,3 +31,6 @@ export * from './roleTypes'
|
||||
|
||||
// 启用状态相关
|
||||
export * from './enableStatus'
|
||||
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
|
||||
@@ -449,12 +449,17 @@
|
||||
"taskManagement": "Task Management",
|
||||
"exportTaskManagement": "Export Management",
|
||||
"exportTaskList": "Export List",
|
||||
"exportDevice": "Export Devices",
|
||||
"exportIotCard": "Export IOT Cards",
|
||||
"exportOrder": "Export Orders",
|
||||
"exportTaskDetail": "Export Task Detail",
|
||||
"exchangeManagement": "Exchange Management",
|
||||
"exchangeDetail": "Exchange Order Detail"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings Management",
|
||||
"paymentSettings": "Payment Settings",
|
||||
"detailsOfPaymentConfiguration": "Payment Configuration Details",
|
||||
"paymentMerchant": "Payment Merchant",
|
||||
"developerApi": "Developer API",
|
||||
"commissionTemplate": "Commission Template"
|
||||
|
||||
@@ -383,6 +383,9 @@
|
||||
"taskManagement": "任务管理",
|
||||
"exportTaskManagement": "导出管理",
|
||||
"exportTaskList": "导出列表",
|
||||
"exportDevice": "导出设备",
|
||||
"exportIotCard": "导出IOT卡",
|
||||
"exportOrder": "导出订单",
|
||||
"exportTaskDetail": "导出任务详情",
|
||||
"exchangeManagement": "换货管理",
|
||||
"exchangeDetail": "换货单详情"
|
||||
@@ -407,7 +410,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置管理",
|
||||
"wechatPayConfiguration": "微信配置",
|
||||
"paymentSettings": "支付设置",
|
||||
"detailsOfPaymentConfiguration": "支付配置详情",
|
||||
"withdrawalSettings": "提现配置",
|
||||
"passwordSettings": "密码设置"
|
||||
|
||||
@@ -331,8 +331,7 @@ function buildRouteMap(routes: AppRouteRecord[], parentPath = ''): Map<string, A
|
||||
*/
|
||||
function convertBackendMenuToRoute(
|
||||
menu: any,
|
||||
routeMap: Map<string, AppRouteRecord>,
|
||||
parentPath = ''
|
||||
routeMap: Map<string, AppRouteRecord>
|
||||
): AppRouteRecord | null {
|
||||
const menuUrl = menu.url || '/'
|
||||
const matchedRoute = routeMap.get(menuUrl)
|
||||
@@ -343,7 +342,7 @@ function convertBackendMenuToRoute(
|
||||
// 如果当前菜单没有匹配的路由,但有 children,尝试递归处理 children
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const children = menu.children
|
||||
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
|
||||
.map((child: any) => convertBackendMenuToRoute(child, routeMap))
|
||||
.filter((child: AppRouteRecord | null) => child !== null)
|
||||
|
||||
// 如果子菜单有有效的路由,返回一个包含子菜单的占位路由
|
||||
@@ -387,7 +386,7 @@ function convertBackendMenuToRoute(
|
||||
// 递归处理后端返回的 children
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const children = menu.children
|
||||
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
|
||||
.map((child: any) => convertBackendMenuToRoute(child, routeMap))
|
||||
.filter((child: AppRouteRecord | null) => child !== null)
|
||||
|
||||
if (children.length > 0) {
|
||||
|
||||
@@ -451,12 +451,32 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'export-task-list',
|
||||
name: 'ExportTaskList',
|
||||
component: RoutesAlias.ExportTaskList,
|
||||
path: 'export-device',
|
||||
name: 'ExportDeviceTaskList',
|
||||
component: RoutesAlias.ExportDeviceTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportTaskList',
|
||||
permissions: ['export_task:list'],
|
||||
title: 'menus.assetManagement.exportDevice',
|
||||
exportTaskScene: 'device',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-iot-card',
|
||||
name: 'ExportIotCardTaskList',
|
||||
component: RoutesAlias.ExportIotCardTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportIotCard',
|
||||
exportTaskScene: 'iot_card',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'export-order',
|
||||
name: 'ExportOrderTaskList',
|
||||
component: RoutesAlias.ExportOrderTaskList,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportOrder',
|
||||
exportTaskScene: 'order',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
@@ -466,7 +486,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
component: RoutesAlias.ExportTaskDetail,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.exportTaskDetail',
|
||||
permissions: ['export_task:detail'],
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
@@ -621,13 +640,13 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
// 微信配置
|
||||
// 支付设置
|
||||
{
|
||||
path: 'wechat-config',
|
||||
name: 'WechatConfig',
|
||||
component: RoutesAlias.WechatConfig,
|
||||
path: 'payment-settings',
|
||||
name: 'PaymentSettings',
|
||||
component: RoutesAlias.PaymentSettings,
|
||||
meta: {
|
||||
title: 'menus.settings.wechatPayConfiguration',
|
||||
title: 'menus.settings.paymentSettings',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
@@ -642,11 +661,11 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 微信支付配置详情
|
||||
// 支付设置详情
|
||||
{
|
||||
path: 'wechat-config/detail/:id',
|
||||
name: 'WechatConfigDetailRoute',
|
||||
component: RoutesAlias.WechatConfigDetail,
|
||||
path: 'payment-settings/detail/:id',
|
||||
name: 'PaymentSettingsDetailRoute',
|
||||
component: RoutesAlias.PaymentSettingsDetail,
|
||||
meta: {
|
||||
title: 'menus.settings.detailsOfPaymentConfiguration',
|
||||
isHide: true,
|
||||
|
||||
@@ -60,7 +60,9 @@ export enum RoutesAlias {
|
||||
OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务
|
||||
OrderPackageInvalidateTaskDetail = '/asset-management/task-management/order-package-invalidate-task/detail', // 订单套餐作废任务详情
|
||||
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
ExportTaskList = '/asset-management/export-task-management/export-task-list', // 导出列表
|
||||
ExportDeviceTaskList = '/asset-management/export-task-management/export-device', // 导出设备
|
||||
ExportIotCardTaskList = '/asset-management/export-task-management/export-iot-card', // 导出IOT卡
|
||||
ExportOrderTaskList = '/asset-management/export-task-management/export-order', // 导出订单
|
||||
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
|
||||
|
||||
// 订单管理
|
||||
@@ -80,8 +82,8 @@ export enum RoutesAlias {
|
||||
|
||||
// 设置管理
|
||||
WithdrawalSettings = '/settings/withdrawal-settings', // 提现配置
|
||||
WechatConfig = '/settings/wechat-config', // 微信配置
|
||||
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
|
||||
PaymentSettings = '/settings/payment-settings', // 支付设置
|
||||
PaymentSettingsDetail = '/settings/payment-settings/detail', // 支付设置详情
|
||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||
|
||||
// 轮询管理
|
||||
|
||||
@@ -42,6 +42,10 @@ export type UsageType = 'single_card' | 'device'
|
||||
|
||||
// ========== 资产详情响应 ==========
|
||||
|
||||
export interface AssetResolveParams {
|
||||
include_usage_summary?: boolean // 是否返回当前世代流量汇总字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 资产解析/详情响应
|
||||
* 对应接口:GET /api/admin/assets/resolve/:identifier
|
||||
@@ -67,6 +71,8 @@ export interface AssetResolveResponse {
|
||||
real_used_mb: number // 真流量已用
|
||||
virtual_total_mb: number // 业务停机阈值(展示用)
|
||||
virtual_used_mb: number // 展示已用量
|
||||
total_virtual_used_mb?: number | null // 所有套餐已用量(MB),未请求时为 null
|
||||
total_virtual_remaining_mb?: number | null // 所有套餐剩余量(MB),未请求时为 null
|
||||
reduction_pct: number // 展示增幅比例(小数,如 0.428571)
|
||||
device_protect_status?: DeviceProtectStatus // 保护期状态:none / stop / start(仅 device)
|
||||
activated_at: string // 激活时间
|
||||
|
||||
@@ -75,8 +75,8 @@ export * from './asset'
|
||||
// 代理充值相关
|
||||
export * from './agentRecharge'
|
||||
|
||||
// 微信支付配置相关
|
||||
export * from './wechatConfig'
|
||||
// 支付设置相关
|
||||
export * from './paymentSettings'
|
||||
|
||||
// 退款管理相关
|
||||
export * from './refund'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 微信支付配置管理相关类型定义
|
||||
* 支付设置相关类型定义
|
||||
*/
|
||||
|
||||
// 支付渠道类型
|
||||
@@ -66,8 +66,8 @@ type PaymentConfigWritableFields = Partial<
|
||||
FuiouConfigFields
|
||||
>
|
||||
|
||||
// 支付配置
|
||||
export interface WechatConfig
|
||||
// 支付设置
|
||||
export interface PaymentSettings
|
||||
extends PaymentConfigBase,
|
||||
MiniappConfigFields,
|
||||
OfficialAccountConfigFields,
|
||||
@@ -76,7 +76,7 @@ export interface WechatConfig
|
||||
FuiouConfigFields {}
|
||||
|
||||
// 查询参数
|
||||
export interface WechatConfigQueryParams {
|
||||
export interface PaymentSettingsQueryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
provider_type?: PaymentProviderType
|
||||
@@ -84,23 +84,23 @@ export interface WechatConfigQueryParams {
|
||||
}
|
||||
|
||||
// 列表响应
|
||||
export interface WechatConfigListResponse {
|
||||
items: WechatConfig[]
|
||||
export interface PaymentSettingsListResponse {
|
||||
items: PaymentSettings[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
// 创建微信支付配置请求
|
||||
export interface CreateWechatConfigRequest extends PaymentConfigWritableFields {
|
||||
// 创建支付设置请求
|
||||
export interface CreatePaymentSettingsRequest extends PaymentConfigWritableFields {
|
||||
name: string
|
||||
provider_type: PaymentProviderType
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 更新微信支付配置请求
|
||||
export interface UpdateWechatConfigRequest extends PaymentConfigWritableFields {
|
||||
// 更新支付设置请求
|
||||
export interface UpdatePaymentSettingsRequest extends PaymentConfigWritableFields {
|
||||
name?: string
|
||||
description?: string
|
||||
provider_type?: PaymentProviderType
|
||||
@@ -20,7 +20,7 @@ export interface PaymentMerchantSetting {
|
||||
publicKey: string
|
||||
notifyUrl?: string
|
||||
}
|
||||
// 微信配置
|
||||
// 微信支付配置
|
||||
wechat?: {
|
||||
enabled: boolean
|
||||
appId: string
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
import type { ExportTaskScene } from '@/types/api'
|
||||
|
||||
// 路由元数据
|
||||
export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
@@ -33,6 +34,8 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
isFirstLevel?: boolean
|
||||
/** 角色权限 */
|
||||
roles?: string[]
|
||||
/** 导出任务固定场景 */
|
||||
exportTaskScene?: ExportTaskScene
|
||||
/** 是否固定标签页 */
|
||||
fixedTab?: boolean
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PaymentProviderType, WechatConfig } from '@/types/api'
|
||||
import type { PaymentProviderType, PaymentSettings } from '@/types/api'
|
||||
|
||||
const PAYMENT_PROVIDER_LABELS: Record<PaymentProviderType, string> = {
|
||||
wechat: '微信直连',
|
||||
@@ -11,7 +11,7 @@ export const getPaymentProviderText = (type: PaymentProviderType): string => {
|
||||
}
|
||||
|
||||
export const getPaymentMerchantId = (
|
||||
config: Pick<WechatConfig, 'provider_type' | 'wx_mch_id' | 'fy_mchnt_cd'>
|
||||
config: Pick<PaymentSettings, 'provider_type' | 'wx_mch_id' | 'fy_mchnt_cd'>
|
||||
): string => {
|
||||
if (config.provider_type === 'wechat' || config.provider_type === 'wechat_v2') {
|
||||
return config.wx_mch_id || '-'
|
||||
@@ -25,7 +25,7 @@ export const getPaymentMerchantId = (
|
||||
}
|
||||
|
||||
export const getPaymentNotifyUrl = (
|
||||
config: Pick<WechatConfig, 'provider_type' | 'wx_notify_url' | 'fy_notify_url' | 'ali_notify_url'>
|
||||
config: Pick<PaymentSettings, 'provider_type' | 'wx_notify_url' | 'fy_notify_url' | 'ali_notify_url'>
|
||||
): string => {
|
||||
if (config.provider_type === 'wechat' || config.provider_type === 'wechat_v2') {
|
||||
return config.wx_notify_url || '-'
|
||||
|
||||
@@ -9,7 +9,7 @@ const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL, // 开发服务器使用代理,其他模式使用完整URL
|
||||
withCredentials: false, // 异步请求携带cookie
|
||||
transformRequest: [
|
||||
(data, headers) => {
|
||||
(data) => {
|
||||
// 如果是 FormData,不进行转换
|
||||
if (data instanceof FormData) {
|
||||
return data
|
||||
@@ -72,6 +72,7 @@ let failedQueue: Array<{
|
||||
resolve: (value?: any) => void
|
||||
reject: (reason?: any) => void
|
||||
}> = [] // 失败队列,存储因 token 过期而失败的请求
|
||||
let isHandlingAuthExpiry = false
|
||||
|
||||
// 处理失败队列
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
@@ -222,6 +223,11 @@ function handleErrorMessage(
|
||||
const backendMessage = error.response?.data?.msg
|
||||
const httpStatus = error.response?.status
|
||||
|
||||
// 401 过期类错误由 clearLocalStateAndRedirect 统一提示,避免重复弹出后端报错。
|
||||
if (httpStatus === 401 || error.response?.data?.code === ApiStatus.unauthorized) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是404错误,且未配置显示404错误,则不显示任何提示
|
||||
if (httpStatus === 404 && !show404Error) {
|
||||
return
|
||||
@@ -306,9 +312,13 @@ const api = {
|
||||
|
||||
// 仅清理本地状态(不调用接口)- 用于401等情况
|
||||
const clearLocalStateAndRedirect = () => {
|
||||
if (isHandlingAuthExpiry) return
|
||||
isHandlingAuthExpiry = true
|
||||
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
setTimeout(() => {
|
||||
useUserStore().clearLocalState()
|
||||
isHandlingAuthExpiry = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,13 @@
|
||||
<ElDescriptionsItem label="IMEI">{{
|
||||
cardInfo?.gateway_card_imei || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商账户">{{
|
||||
cardInfo?.carrier_name || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商账户">{{ carrierAccountDisplay }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实名状态">
|
||||
<ElTag :type="getRealNameStatusType(cardInfo?.real_name_status)" size="small">
|
||||
{{ getRealNameStatusName(cardInfo?.real_name_status) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实名认证策略">
|
||||
<ElDescriptionsItem v-if="canShowSensitiveFields" label="实名认证策略">
|
||||
{{ getRealnamePolicyName(cardInfo?.realname_policy) }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
@@ -82,6 +80,12 @@
|
||||
<ElDescriptionsItem label="运营商状态" :span="1">
|
||||
<span class="gateway-extend-text">{{ cardInfo?.gateway_extend || '-' }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所有套餐已用量">
|
||||
{{ formatUsageSummaryMb(cardInfo?.total_virtual_used_mb) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所有套餐剩余量">
|
||||
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<template v-if="cardInfo?.bound_device_id">
|
||||
<ElDescriptionsItem label="绑定设备号">
|
||||
@@ -151,18 +155,26 @@
|
||||
<ElDescriptionsItem label="最大卡槽数">{{
|
||||
cardInfo?.max_sim_slots || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实名认证策略">
|
||||
<ElDescriptionsItem v-if="canShowSensitiveFields" label="实名认证策略">
|
||||
{{ getRealnamePolicyName(cardInfo?.realname_policy) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型">{{ cardInfo?.device_type || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="制造商">{{ cardInfo?.manufacturer || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="canShowSensitiveFields" label="制造商">{{
|
||||
cardInfo?.manufacturer || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后在线时间">{{
|
||||
formatDateTime(cardInfo?.last_online_time) || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="SN">{{ cardInfo?.sn || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所有套餐已用量">
|
||||
{{ formatUsageSummaryMb(cardInfo?.total_virtual_used_mb) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所有套餐剩余量">
|
||||
{{ formatUsageSummaryMb(cardInfo?.total_virtual_remaining_mb) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后同步时间">{{
|
||||
formatDateTime(deviceRealtime?.last_sync_time) || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="SN">{{ cardInfo?.sn || '-' }}</ElDescriptionsItem>
|
||||
</template>
|
||||
</ElDescriptions>
|
||||
|
||||
@@ -512,6 +524,7 @@
|
||||
iccid?: string
|
||||
imei?: string
|
||||
msisdn?: string
|
||||
carrier_type?: string
|
||||
carrier_name?: string
|
||||
real_name_status?: number
|
||||
activation_status_name?: string
|
||||
@@ -532,6 +545,8 @@
|
||||
supplier?: string
|
||||
gateway_card_imei?: string
|
||||
gateway_extend?: string
|
||||
total_virtual_used_mb?: number | null
|
||||
total_virtual_remaining_mb?: number | null
|
||||
last_sync_time?: string | null
|
||||
last_data_check_at?: string | null
|
||||
last_real_name_check_at?: string | null
|
||||
@@ -629,6 +644,22 @@
|
||||
return ![3, 4].includes(userType) && hasAuth(CARD_MONTH_USAGE_PERMISSION)
|
||||
})
|
||||
|
||||
const isAgentOrEnterpriseAccount = computed(() => {
|
||||
const userType = Number(userStore.info.user_type ?? 0)
|
||||
return [3, 4].includes(userType)
|
||||
})
|
||||
|
||||
const canShowSensitiveFields = computed(() => {
|
||||
return !isAgentOrEnterpriseAccount.value
|
||||
})
|
||||
|
||||
const carrierAccountDisplay = computed(() => {
|
||||
if (isAgentOrEnterpriseAccount.value) {
|
||||
return getCarrierTypeName(props.cardInfo?.carrier_type)
|
||||
}
|
||||
return props.cardInfo?.carrier_name || '-'
|
||||
})
|
||||
|
||||
const descriptionsColumn = computed(() => {
|
||||
if (width.value <= 640) return 1
|
||||
if (width.value <= 1024) return 2
|
||||
@@ -665,6 +696,7 @@
|
||||
getRealNameStatusName,
|
||||
getRealNameStatusType,
|
||||
getRealnamePolicyName,
|
||||
getCarrierTypeName,
|
||||
getAssetStatusName,
|
||||
getAssetStatusType,
|
||||
getOnlineStatusName,
|
||||
@@ -710,6 +742,11 @@
|
||||
props.cardInfo?.last_card_status_check_at !== undefined
|
||||
)
|
||||
|
||||
const formatUsageSummaryMb = (value?: number | null) => {
|
||||
if (value === undefined || value === null) return '-'
|
||||
return formatDataSize(value)
|
||||
}
|
||||
|
||||
// 处理轮询开关变化
|
||||
const handlePollingChange = (value: string | number | boolean) => {
|
||||
emit('update:pollingEnabled', Boolean(value))
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* 资产信息格式化工具函数
|
||||
*/
|
||||
|
||||
import { getCarrierTypeLabel } from '@/config/constants/carrierTypes'
|
||||
import type { CarrierType } from '@/types/api'
|
||||
|
||||
type TagType = 'primary' | 'success' | 'warning' | 'info' | 'danger'
|
||||
|
||||
export function useAssetFormatters() {
|
||||
@@ -76,6 +79,13 @@ export function useAssetFormatters() {
|
||||
return policyMap[policy] || '未知'
|
||||
}
|
||||
|
||||
// 获取运营商类型名称
|
||||
const getCarrierTypeName = (carrierType?: string | null) => {
|
||||
const normalizedCarrierType = carrierType?.trim()
|
||||
if (!normalizedCarrierType) return '-'
|
||||
return getCarrierTypeLabel(normalizedCarrierType as CarrierType)
|
||||
}
|
||||
|
||||
// 获取资产状态名称
|
||||
const getAssetStatusName = (status?: number) => {
|
||||
if (status === undefined || status === null) return '未知'
|
||||
@@ -252,6 +262,7 @@ export function useAssetFormatters() {
|
||||
getRealNameStatusName,
|
||||
getRealNameStatusType,
|
||||
getRealnamePolicyName,
|
||||
getCarrierTypeName,
|
||||
getAssetStatusName,
|
||||
getAssetStatusType,
|
||||
getOnlineStatusName,
|
||||
|
||||
@@ -92,7 +92,7 @@ export function useAssetInfo() {
|
||||
const fetchAssetDetail = async (identifier: string) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await AssetService.resolveAsset(identifier)
|
||||
const response = await AssetService.resolveAsset(identifier, { include_usage_summary: true })
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
const data = response.data
|
||||
@@ -136,6 +136,8 @@ export function useAssetInfo() {
|
||||
gateway_card_imei: resolvedGatewayCardImei,
|
||||
resolved_gateway_card_imei: resolvedGatewayCardImei,
|
||||
gateway_extend: data.gateway_extend ?? '',
|
||||
total_virtual_used_mb: data.total_virtual_used_mb ?? null,
|
||||
total_virtual_remaining_mb: data.total_virtual_remaining_mb ?? null,
|
||||
bound_device_id: data.bound_device_id,
|
||||
bound_device_no: data.bound_device_no || '',
|
||||
bound_device_name: data.bound_device_name || '',
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface AssetInfo {
|
||||
iccid?: string
|
||||
imei?: string
|
||||
msisdn?: string
|
||||
carrier_type?: string
|
||||
carrier_name?: string
|
||||
real_name_status?: number
|
||||
activation_status_name?: string
|
||||
@@ -30,6 +31,8 @@ export interface AssetInfo {
|
||||
status?: number
|
||||
gateway_card_imei?: string
|
||||
gateway_extend?: string
|
||||
total_virtual_used_mb?: number | null
|
||||
total_virtual_remaining_mb?: number | null
|
||||
last_sync_time?: string | null
|
||||
last_data_check_at?: string | null
|
||||
last_real_name_check_at?: string | null
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<ExportTaskList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ExportTaskList from '../export-task-list/index.vue'
|
||||
|
||||
defineOptions({ name: 'ExportDeviceTaskList' })
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<ExportTaskList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ExportTaskList from '../export-task-list/index.vue'
|
||||
|
||||
defineOptions({ name: 'ExportIotCardTaskList' })
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<ExportTaskList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ExportTaskList from '../export-task-list/index.vue'
|
||||
|
||||
defineOptions({ name: 'ExportOrderTaskList' })
|
||||
</script>
|
||||
@@ -10,23 +10,11 @@
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">导出任务详情</h2>
|
||||
<div class="detail-actions" v-if="taskDetail">
|
||||
<ElButton
|
||||
v-if="
|
||||
taskDetail.status === ExportTaskStatus.COMPLETED && hasAuth('export_task:download')
|
||||
"
|
||||
type="primary"
|
||||
@click="downloadTask"
|
||||
>
|
||||
<div class="detail-actions" v-if="taskDetail && canViewDetail">
|
||||
<ElButton v-if="canDownloadTask" type="primary" @click="downloadTask">
|
||||
下载文件
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="canCancelTask && hasAuth('export_task:cancel')"
|
||||
type="danger"
|
||||
@click="cancelTask"
|
||||
>
|
||||
取消任务
|
||||
</ElButton>
|
||||
<ElButton v-if="canCancelTask" type="danger" @click="cancelTask"> 取消任务 </ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,8 +24,12 @@
|
||||
</ElIcon>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<DetailPage v-else-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
||||
<ElEmpty v-else description="暂无详情" />
|
||||
<DetailPage
|
||||
v-else-if="taskDetail && canViewDetail"
|
||||
:sections="detailSections"
|
||||
:data="taskDetail"
|
||||
/>
|
||||
<ElEmpty v-else :description="forbidden ? '暂无权限查看该导出任务详情' : '暂无详情'" />
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -52,8 +44,9 @@
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { ExportTaskService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { getExportTaskSceneConfig, getExportTaskSceneName } from '@/config/constants'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { ExportTaskDetail, ExportTaskScene } from '@/types/api'
|
||||
import type { ExportTaskDetail } from '@/types/api'
|
||||
import { ExportTaskStatus } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'ExportTaskDetail' })
|
||||
@@ -62,17 +55,22 @@
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const taskDetail = ref<ExportTaskDetail | null>(null)
|
||||
|
||||
const getSceneName = (scene?: ExportTaskScene) => {
|
||||
const sceneMap: Record<ExportTaskScene, string> = {
|
||||
device: '设备管理',
|
||||
iot_card: 'IoT卡管理',
|
||||
order: '订单管理'
|
||||
}
|
||||
|
||||
return scene ? sceneMap[scene] : '-'
|
||||
}
|
||||
const taskPermissions = computed(
|
||||
() => getExportTaskSceneConfig(taskDetail.value?.scene)?.permissions
|
||||
)
|
||||
const canViewDetail = computed(
|
||||
() => !!taskPermissions.value && hasAuth(taskPermissions.value.detail)
|
||||
)
|
||||
const canDownloadTask = computed(
|
||||
() =>
|
||||
canViewDetail.value &&
|
||||
taskDetail.value?.status === ExportTaskStatus.COMPLETED &&
|
||||
!!taskPermissions.value &&
|
||||
hasAuth(taskPermissions.value.download)
|
||||
)
|
||||
|
||||
const getStatusTagType = (status?: ExportTaskStatus) => {
|
||||
const statusTypeMap: Record<
|
||||
@@ -89,10 +87,14 @@
|
||||
return status ? statusTypeMap[status] : 'info'
|
||||
}
|
||||
|
||||
const canCancelTask = computed(() =>
|
||||
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(
|
||||
taskDetail.value?.status as ExportTaskStatus
|
||||
)
|
||||
const canCancelTask = computed(
|
||||
() =>
|
||||
canViewDetail.value &&
|
||||
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(
|
||||
taskDetail.value?.status as ExportTaskStatus
|
||||
) &&
|
||||
!!taskPermissions.value &&
|
||||
hasAuth(taskPermissions.value.cancel)
|
||||
)
|
||||
|
||||
const detailSections = computed((): DetailSection[] => [
|
||||
@@ -102,7 +104,7 @@
|
||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '导出场景',
|
||||
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getSceneName(data.scene))
|
||||
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getExportTaskSceneName(data.scene))
|
||||
},
|
||||
{
|
||||
label: '任务状态',
|
||||
@@ -174,12 +176,13 @@
|
||||
fullWidth: true,
|
||||
render: (data: ExportTaskDetail) => {
|
||||
if (!data.download_url) return h('span', '-')
|
||||
if (!canDownloadTask.value) return h('span', '-')
|
||||
return h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => window.open(data.download_url, '_blank')
|
||||
onClick: () => downloadTask()
|
||||
},
|
||||
() => '打开下载地址'
|
||||
)
|
||||
@@ -209,10 +212,15 @@
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
forbidden.value = false
|
||||
try {
|
||||
const res = await ExportTaskService.getExportTaskDetail(taskId)
|
||||
if (res.code === 0) {
|
||||
taskDetail.value = res.data
|
||||
if (!canViewDetail.value) {
|
||||
forbidden.value = true
|
||||
ElMessage.warning('您没有查看该导出任务详情的权限')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取导出任务详情失败')
|
||||
}
|
||||
@@ -225,6 +233,11 @@
|
||||
}
|
||||
|
||||
const downloadTask = () => {
|
||||
if (!canDownloadTask.value) {
|
||||
ElMessage.warning('您没有下载该导出任务的权限')
|
||||
return
|
||||
}
|
||||
|
||||
if (!taskDetail.value?.download_url) {
|
||||
ElMessage.warning('当前任务暂无可用下载地址')
|
||||
return
|
||||
@@ -235,6 +248,10 @@
|
||||
|
||||
const cancelTask = () => {
|
||||
if (!taskDetail.value) return
|
||||
if (!canCancelTask.value) {
|
||||
ElMessage.warning('您没有取消该导出任务的权限')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确定取消导出任务 ${taskDetail.value.task_no} 吗?`, '取消确认', {
|
||||
confirmButtonText: '确定',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="110"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
@@ -38,27 +39,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, ElProgress, ElTag } from 'element-plus'
|
||||
import { ExportTaskService } from '@/api/modules'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getExportTaskSceneConfig } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { ExportTaskItem, ExportTaskScene } from '@/types/api'
|
||||
import { ExportTaskStatus } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'ExportTaskList' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const taskList = ref<ExportTaskItem[]>([])
|
||||
const defaultSceneConfig = getExportTaskSceneConfig('device')!
|
||||
|
||||
const routeScene = computed(() => route.meta.exportTaskScene as ExportTaskScene | undefined)
|
||||
const sceneConfig = computed(
|
||||
() => getExportTaskSceneConfig(routeScene.value) || defaultSceneConfig
|
||||
)
|
||||
const currentScene = computed(() => sceneConfig.value.scene)
|
||||
const currentPermissions = computed(() => sceneConfig.value.permissions)
|
||||
|
||||
const initialSearchState = {
|
||||
scene: undefined as ExportTaskScene | undefined,
|
||||
status: undefined as ExportTaskStatus | undefined,
|
||||
dateRange: [] as string[],
|
||||
start_time: '',
|
||||
@@ -72,12 +82,6 @@
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sceneOptions = [
|
||||
{ label: '设备管理', value: 'device' },
|
||||
{ label: 'IoT卡管理', value: 'iot_card' },
|
||||
{ label: '订单管理', value: 'order' }
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '待处理', value: ExportTaskStatus.PENDING },
|
||||
{ label: '处理中', value: ExportTaskStatus.PROCESSING },
|
||||
@@ -87,13 +91,6 @@
|
||||
]
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '导出场景',
|
||||
prop: 'scene',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '请选择导出场景' },
|
||||
options: () => sceneOptions
|
||||
},
|
||||
{
|
||||
label: '任务状态',
|
||||
prop: 'status',
|
||||
@@ -117,7 +114,6 @@
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '场景', prop: 'scene' },
|
||||
{ label: '状态', prop: 'status_name' },
|
||||
{ label: '进度', prop: 'progress' },
|
||||
{ label: '格式', prop: 'format' },
|
||||
@@ -144,12 +140,6 @@
|
||||
row.task_no || '-'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'scene',
|
||||
label: '场景',
|
||||
width: 120,
|
||||
formatter: (row: ExportTaskItem) => getSceneName(row.scene)
|
||||
},
|
||||
{
|
||||
prop: 'status_name',
|
||||
label: '状态',
|
||||
@@ -181,11 +171,6 @@
|
||||
}
|
||||
])
|
||||
|
||||
const getSceneName = (scene?: ExportTaskScene) => {
|
||||
const option = sceneOptions.find((item) => item.value === scene)
|
||||
return option?.label || '-'
|
||||
}
|
||||
|
||||
const getStatusTagType = (status: ExportTaskStatus) => {
|
||||
const statusTypeMap: Record<
|
||||
ExportTaskStatus,
|
||||
@@ -218,7 +203,7 @@
|
||||
const res = await ExportTaskService.getExportTasks({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
scene: searchForm.scene,
|
||||
scene: currentScene.value,
|
||||
status: searchForm.status,
|
||||
start_time: searchForm.start_time || undefined,
|
||||
end_time: searchForm.end_time || undefined
|
||||
@@ -272,8 +257,12 @@
|
||||
return res.data
|
||||
}
|
||||
|
||||
const getRowPermissions = (row: ExportTaskItem) => {
|
||||
return getExportTaskSceneConfig(row.scene)?.permissions || currentPermissions.value
|
||||
}
|
||||
|
||||
const goDetail = (row: ExportTaskItem) => {
|
||||
if (!hasAuth('export_task:detail')) {
|
||||
if (!hasAuth(getRowPermissions(row).detail)) {
|
||||
ElMessage.warning('您没有查看导出任务详情的权限')
|
||||
return
|
||||
}
|
||||
@@ -285,6 +274,11 @@
|
||||
}
|
||||
|
||||
const downloadTask = async (row: ExportTaskItem) => {
|
||||
if (!hasAuth(getRowPermissions(row).download)) {
|
||||
ElMessage.warning('您没有下载导出任务的权限')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const detail = await getTaskDetail(row.id)
|
||||
if (!detail.download_url) {
|
||||
@@ -300,6 +294,11 @@
|
||||
}
|
||||
|
||||
const cancelTask = (row: ExportTaskItem) => {
|
||||
if (!hasAuth(getRowPermissions(row).cancel)) {
|
||||
ElMessage.warning('您没有取消导出任务的权限')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(`确定取消导出任务 ${row.task_no} 吗?`, '取消确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
@@ -322,14 +321,15 @@
|
||||
|
||||
const getActions = (row: ExportTaskItem) => {
|
||||
const actions: any[] = []
|
||||
const permissions = getRowPermissions(row)
|
||||
|
||||
if (row.status === ExportTaskStatus.COMPLETED && hasAuth('export_task:download')) {
|
||||
if (row.status === ExportTaskStatus.COMPLETED && hasAuth(permissions.download)) {
|
||||
actions.push({ label: '下载', handler: () => downloadTask(row), type: 'primary' })
|
||||
}
|
||||
|
||||
if (
|
||||
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(row.status) &&
|
||||
hasAuth('export_task:cancel')
|
||||
hasAuth(permissions.cancel)
|
||||
) {
|
||||
actions.push({ label: '取消', handler: () => cancelTask(row), type: 'danger' })
|
||||
}
|
||||
@@ -340,6 +340,12 @@
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
watch(currentScene, () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -991,12 +991,13 @@
|
||||
:identifier="operationLogsIdentifier"
|
||||
download-permission="iot_card:download_log_file"
|
||||
/>
|
||||
|
||||
<ExportTaskCreateDialog
|
||||
v-model="exportDialogVisible"
|
||||
scene="iot_card"
|
||||
:query="exportQuery"
|
||||
confirm-permission="iot_card:export"
|
||||
title="导出IoT卡"
|
||||
title="导出IOT卡"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<CommissionSummary v-if="hasPermission('dashboard_analysis:commission_summary')" />
|
||||
<CommissionStats v-if="hasPermission('dashboard_analysis:commission_stats')" />
|
||||
<WithdrawalSettings v-if="hasPermission('dashboard_analysis:withdrawal_settings')" />
|
||||
<ActiveWechatConfig v-if="hasPermission('dashboard_analysis:wechat_config')" />
|
||||
<ActivePaymentSettings v-if="hasPermission('dashboard_analysis:payment_settings')" />
|
||||
|
||||
<!--<el-row :gutter="20">-->
|
||||
<!-- <el-col :xl="14" :lg="15" :xs="24">-->
|
||||
@@ -46,7 +46,7 @@
|
||||
import CommissionSummary from './widget/CommissionSummary.vue'
|
||||
import CommissionStats from './widget/CommissionStats.vue'
|
||||
import WithdrawalSettings from './widget/WithdrawalSettings.vue'
|
||||
import ActiveWechatConfig from './widget/ActiveWechatConfig.vue'
|
||||
import ActivePaymentSettings from './widget/ActivePaymentSettings.vue'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
defineOptions({ name: 'Analysis' })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ElCard shadow="never" class="active-wechat-config-widget">
|
||||
<ElCard shadow="never" class="active-payment-settings-widget">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
@@ -94,9 +94,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { WechatConfigService } from '@/api/modules'
|
||||
import { PaymentSettingsService } from '@/api/modules'
|
||||
import { ElTag } from 'element-plus'
|
||||
import type { WechatConfig } from '@/types/api'
|
||||
import type { PaymentSettings } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
getPaymentConfigStatus,
|
||||
@@ -106,26 +106,26 @@
|
||||
} from '@/utils/business/paymentConfig'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
defineOptions({ name: 'ActiveWechatConfigWidget' })
|
||||
defineOptions({ name: 'ActivePaymentSettingsWidget' })
|
||||
|
||||
const { hasPermission } = usePermission()
|
||||
|
||||
// 当前生效的支付配置
|
||||
const activeConfig = ref<WechatConfig | null>(null)
|
||||
const activeConfig = ref<PaymentSettings | null>(null)
|
||||
|
||||
const getProviderTypeText = (type: WechatConfig['provider_type']): string => {
|
||||
const getProviderTypeText = (type: PaymentSettings['provider_type']): string => {
|
||||
return getPaymentProviderText(type)
|
||||
}
|
||||
|
||||
const getMerchantId = (config: WechatConfig): string => {
|
||||
const getMerchantId = (config: PaymentSettings): string => {
|
||||
return getPaymentMerchantId(config)
|
||||
}
|
||||
|
||||
const getNotifyUrl = (config: WechatConfig): string => {
|
||||
const getNotifyUrl = (config: PaymentSettings): string => {
|
||||
return getPaymentNotifyUrl(config)
|
||||
}
|
||||
|
||||
const getCredentialStatus = (config: WechatConfig): string => {
|
||||
const getCredentialStatus = (config: PaymentSettings): string => {
|
||||
if (config.provider_type === 'wechat' || config.provider_type === 'wechat_v2') {
|
||||
const certStatus = getPaymentConfigStatus(config.wx_cert_content)
|
||||
const keyStatus = getPaymentConfigStatus(config.wx_key_content)
|
||||
@@ -156,12 +156,12 @@
|
||||
// 加载当前生效配置
|
||||
const loadActiveConfig = async () => {
|
||||
// 检查权限,没有权限不调用接口
|
||||
if (!hasPermission('dashboard_analysis:wechat_config')) {
|
||||
if (!hasPermission('dashboard_analysis:payment_settings')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await WechatConfigService.getActiveWechatConfig()
|
||||
const res = await PaymentSettingsService.getActivePaymentSettings()
|
||||
if (res.code === 0 && res.data) {
|
||||
activeConfig.value = res.data
|
||||
}
|
||||
@@ -176,7 +176,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.active-wechat-config-widget {
|
||||
.active-payment-settings-widget {
|
||||
:deep(.el-card__header) {
|
||||
padding: 18px 20px;
|
||||
background: var(--el-fill-color-light);
|
||||
@@ -276,7 +276,7 @@
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.active-wechat-config-widget {
|
||||
.active-payment-settings-widget {
|
||||
.card-header {
|
||||
.header-left,
|
||||
.header-right {
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="grant-package-list-page">
|
||||
<div class="grant-package-list-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
@@ -27,9 +27,9 @@
|
||||
:loading="loading"
|
||||
:data="packageList"
|
||||
:marginTop="10"
|
||||
:pagination="false"
|
||||
:actions="getActions"
|
||||
:actionsWidth="150"
|
||||
height="70vh"
|
||||
:inlineActionsCount="2"
|
||||
>
|
||||
<template #default>
|
||||
@@ -134,7 +134,6 @@
|
||||
ElCard,
|
||||
ElButton,
|
||||
ElIcon,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
ElDialog,
|
||||
@@ -429,8 +428,23 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.grant-package-list-page {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
:deep(.art-table-card) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.el-card__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="wechat-config-detail-page">
|
||||
<div class="payment-settings-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
@@ -31,18 +31,18 @@
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { WechatConfigService } from '@/api/modules'
|
||||
import type { WechatConfig } from '@/types/api'
|
||||
import { PaymentSettingsService } from '@/api/modules'
|
||||
import type { PaymentSettings } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getPaymentConfigStatus, getPaymentProviderText } from '@/utils/business/paymentConfig'
|
||||
|
||||
defineOptions({ name: 'WechatConfigDetail' })
|
||||
defineOptions({ name: 'PaymentSettingsDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<WechatConfig | null>(null)
|
||||
const detailData = ref<PaymentSettings | null>(null)
|
||||
|
||||
const configId = computed(() => Number(route.params.id))
|
||||
const pageTitle = computed(() => `支付配置详情 #${configId.value}`)
|
||||
@@ -176,9 +176,9 @@
|
||||
]
|
||||
}
|
||||
|
||||
// 微信配置
|
||||
// 微信支付配置
|
||||
const wechatPaySection: DetailSection = {
|
||||
title: '微信配置',
|
||||
title: '微信支付配置',
|
||||
fields: [
|
||||
{
|
||||
label: '微信商户号',
|
||||
@@ -289,7 +289,7 @@
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await WechatConfigService.getWechatConfigById(configId.value)
|
||||
const res = await PaymentSettingsService.getPaymentSettingsById(configId.value)
|
||||
if (res.code === 0) {
|
||||
detailData.value = res.data
|
||||
}
|
||||
@@ -306,7 +306,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wechat-config-detail-page {
|
||||
.payment-settings-detail-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="wechat-config-page" id="table-full-screen">
|
||||
<div class="payment-settings-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
@@ -22,7 +22,7 @@
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="showCreateDialog"
|
||||
v-if="hasAuth('wechat_config:create')"
|
||||
v-if="hasAuth('payment_settings:create')"
|
||||
>新增支付配置</ElButton
|
||||
>
|
||||
</template>
|
||||
@@ -226,7 +226,7 @@
|
||||
|
||||
<template v-if="form.provider_type === 'wechat' || form.provider_type === 'wechat_v2'">
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">微信配置</span>
|
||||
<span class="divider-title">微信支付配置</span>
|
||||
</ElDivider>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
@@ -361,15 +361,15 @@
|
||||
<script setup lang="ts">
|
||||
import { h, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { WechatConfigService } from '@/api/modules'
|
||||
import { PaymentSettingsService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
CreateWechatConfigRequest,
|
||||
CreatePaymentSettingsRequest,
|
||||
PaymentProviderType,
|
||||
UpdateWechatConfigRequest,
|
||||
WechatConfig,
|
||||
WechatConfigQueryParams
|
||||
UpdatePaymentSettingsRequest,
|
||||
PaymentSettings,
|
||||
PaymentSettingsQueryParams
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -383,7 +383,7 @@
|
||||
isMaskedPaymentConfigValue
|
||||
} from '@/utils/business/paymentConfig'
|
||||
|
||||
defineOptions({ name: 'WechatConfigList' })
|
||||
defineOptions({ name: 'PaymentSettingsList' })
|
||||
|
||||
type PaymentConfigFormModel = {
|
||||
id: number
|
||||
@@ -455,7 +455,7 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
const canUpdateWechatConfigStatus = hasAuth('wechat_config:status')
|
||||
const canUpdatePaymentSettingsStatus = hasAuth('payment_settings:status')
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
@@ -463,7 +463,7 @@
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const configList = ref<WechatConfig[]>([])
|
||||
const configList = ref<PaymentSettings[]>([])
|
||||
const originalFormState = ref<PaymentConfigFormModel | null>(null)
|
||||
|
||||
const createInitialFormState = (): PaymentConfigFormModel => ({
|
||||
@@ -501,7 +501,7 @@
|
||||
fy_public_key: ''
|
||||
})
|
||||
|
||||
const mapConfigToForm = (detail: WechatConfig): PaymentConfigFormModel => ({
|
||||
const mapConfigToForm = (detail: PaymentSettings): PaymentConfigFormModel => ({
|
||||
...createInitialFormState(),
|
||||
...detail,
|
||||
description: detail.description || '',
|
||||
@@ -666,7 +666,7 @@
|
||||
label: '配置名称',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => {
|
||||
formatter: (row: PaymentSettings) => {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
@@ -685,20 +685,20 @@
|
||||
label: '配置描述',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => row.description || '-'
|
||||
formatter: (row: PaymentSettings) => row.description || '-'
|
||||
},
|
||||
{
|
||||
prop: 'provider_type',
|
||||
label: '支付渠道类型',
|
||||
width: 130,
|
||||
formatter: (row: WechatConfig) => getPaymentProviderText(row.provider_type)
|
||||
formatter: (row: PaymentSettings) => getPaymentProviderText(row.provider_type)
|
||||
},
|
||||
{
|
||||
prop: 'is_active',
|
||||
label: '激活状态',
|
||||
width: 110,
|
||||
formatter: (row: WechatConfig) => {
|
||||
if (canUpdateWechatConfigStatus) {
|
||||
formatter: (row: PaymentSettings) => {
|
||||
if (canUpdatePaymentSettingsStatus) {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.is_active,
|
||||
activeText: '已激活',
|
||||
@@ -718,47 +718,47 @@
|
||||
label: '商户号',
|
||||
width: 150,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => getPaymentMerchantId(row)
|
||||
formatter: (row: PaymentSettings) => getPaymentMerchantId(row)
|
||||
},
|
||||
{
|
||||
prop: 'miniapp_app_id',
|
||||
label: '小程序AppID',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => row.miniapp_app_id || '-'
|
||||
formatter: (row: PaymentSettings) => row.miniapp_app_id || '-'
|
||||
},
|
||||
{
|
||||
prop: 'oa_app_id',
|
||||
label: '公众号AppID',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => row.oa_app_id || '-'
|
||||
formatter: (row: PaymentSettings) => row.oa_app_id || '-'
|
||||
},
|
||||
{
|
||||
prop: 'ali_app_id',
|
||||
label: '支付宝AppID',
|
||||
width: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => row.ali_app_id || '-'
|
||||
formatter: (row: PaymentSettings) => row.ali_app_id || '-'
|
||||
},
|
||||
{
|
||||
prop: 'notify_url',
|
||||
label: '支付回调地址',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: WechatConfig) => getPaymentNotifyUrl(row)
|
||||
formatter: (row: PaymentSettings) => getPaymentNotifyUrl(row)
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: WechatConfig) => formatDateTime(row.created_at)
|
||||
formatter: (row: PaymentSettings) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: WechatConfig) => formatDateTime(row.updated_at)
|
||||
formatter: (row: PaymentSettings) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
@@ -770,14 +770,14 @@
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: WechatConfigQueryParams = {
|
||||
const params: PaymentSettingsQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
provider_type: searchForm.provider_type,
|
||||
is_active: searchForm.is_active !== undefined ? searchForm.is_active === 1 : undefined
|
||||
}
|
||||
|
||||
const res = await WechatConfigService.getWechatConfigs(params)
|
||||
const res = await PaymentSettingsService.getPaymentSettings(params)
|
||||
if (res.code === 0) {
|
||||
configList.value = res.data.items || []
|
||||
pagination.page = res.data.page || pagination.page
|
||||
@@ -825,9 +825,9 @@
|
||||
})
|
||||
}
|
||||
|
||||
const showEditDialog = async (row: WechatConfig) => {
|
||||
const showEditDialog = async (row: PaymentSettings) => {
|
||||
try {
|
||||
const res = await WechatConfigService.getWechatConfigById(row.id)
|
||||
const res = await PaymentSettingsService.getPaymentSettingsById(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
const mappedForm = mapConfigToForm(res.data)
|
||||
Object.assign(form, mappedForm)
|
||||
@@ -853,8 +853,8 @@
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const buildCreatePayload = (): CreateWechatConfigRequest => {
|
||||
const payload: CreateWechatConfigRequest = {
|
||||
const buildCreatePayload = (): CreatePaymentSettingsRequest => {
|
||||
const payload: CreatePaymentSettingsRequest = {
|
||||
name: trimString(form.name),
|
||||
provider_type: form.provider_type,
|
||||
ali_pay_expire_minutes: form.ali_pay_expire_minutes,
|
||||
@@ -878,11 +878,11 @@
|
||||
return payload
|
||||
}
|
||||
|
||||
const buildUpdatePayload = (): UpdateWechatConfigRequest => {
|
||||
const buildUpdatePayload = (): UpdatePaymentSettingsRequest => {
|
||||
const initial = originalFormState.value
|
||||
if (!initial) return {}
|
||||
|
||||
const payload: UpdateWechatConfigRequest = {}
|
||||
const payload: UpdatePaymentSettingsRequest = {}
|
||||
const currentName = trimString(form.name)
|
||||
const initialName = trimString(initial.name)
|
||||
if (currentName !== initialName) {
|
||||
@@ -936,7 +936,7 @@
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
await WechatConfigService.createWechatConfig(buildCreatePayload())
|
||||
await PaymentSettingsService.createPaymentSettings(buildCreatePayload())
|
||||
ElMessage.success('创建成功')
|
||||
} else {
|
||||
const payload = buildUpdatePayload()
|
||||
@@ -945,7 +945,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
await WechatConfigService.updateWechatConfig(form.id, payload)
|
||||
await PaymentSettingsService.updatePaymentSettings(form.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
@@ -958,8 +958,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleStatusChange = async (row: WechatConfig, newStatus: string | number | boolean) => {
|
||||
if (!hasAuth('wechat_config:status')) {
|
||||
const handleStatusChange = async (row: PaymentSettings, newStatus: string | number | boolean) => {
|
||||
if (!hasAuth('payment_settings:status')) {
|
||||
ElMessage.warning('您没有修改激活状态的权限')
|
||||
return
|
||||
}
|
||||
@@ -969,10 +969,10 @@
|
||||
row.is_active = newBoolStatus
|
||||
try {
|
||||
if (newBoolStatus) {
|
||||
await WechatConfigService.activateWechatConfig(row.id)
|
||||
await PaymentSettingsService.activatePaymentSettings(row.id)
|
||||
ElMessage.success('激活成功')
|
||||
} else {
|
||||
await WechatConfigService.deactivateWechatConfig(row.id)
|
||||
await PaymentSettingsService.deactivatePaymentSettings(row.id)
|
||||
ElMessage.success('停用成功')
|
||||
}
|
||||
await getTableData()
|
||||
@@ -982,7 +982,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: WechatConfig) => {
|
||||
const handleDelete = async (row: PaymentSettings) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除支付配置"${row.name}"吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -990,7 +990,7 @@
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await WechatConfigService.deleteWechatConfig(row.id)
|
||||
await PaymentSettingsService.deletePaymentSettings(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
@@ -1000,24 +1000,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: WechatConfig) => {
|
||||
const handleViewDetail = (row: PaymentSettings) => {
|
||||
router.push({
|
||||
path: `${RoutesAlias.WechatConfig}/detail/${row.id}`
|
||||
path: `${RoutesAlias.PaymentSettings}/detail/${row.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleNameClick = (row: WechatConfig) => {
|
||||
if (hasAuth('wechat_config:detail')) {
|
||||
const handleNameClick = (row: PaymentSettings) => {
|
||||
if (hasAuth('payment_settings:detail')) {
|
||||
handleViewDetail(row)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: WechatConfig) => {
|
||||
const getActions = (row: PaymentSettings) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('wechat_config:edit')) {
|
||||
if (hasAuth('payment_settings:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showEditDialog(row),
|
||||
@@ -1025,7 +1025,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('wechat_config:delete')) {
|
||||
if (hasAuth('payment_settings:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleDelete(row),
|
||||
@@ -1042,7 +1042,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wechat-config-page {
|
||||
.payment-settings-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
:data="permissionList"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
:pagination="false"
|
||||
:marginTop="10"
|
||||
:show-pagination="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
|
||||
Reference in New Issue
Block a user