fix: bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m4s

This commit is contained in:
sexygoat
2026-05-11 15:51:50 +08:00
parent 766497dc31
commit 1990a3ccba
40 changed files with 832 additions and 127 deletions

2
.env
View File

@@ -9,7 +9,7 @@ VITE_PORT=3006
VITE_BASE_URL= VITE_BASE_URL=
# API 地址前缀 # API 地址前缀
VITE_API_URL=https://cmp-api.boss160.cn VITE_API_URL=https://cmp-api.xm-iot.cn
# 权限模式( frontend backend # 权限模式( frontend backend
VITE_ACCESS_MODE=backend VITE_ACCESS_MODE=backend

View File

@@ -7,4 +7,4 @@ VITE_BASE_URL=/
VITE_API_URL=https://cmp-api.boss160.cn VITE_API_URL=https://cmp-api.boss160.cn
# Delete console # Delete console
VITE_DROP_CONSOLE = false VITE_DROP_CONSOLE=false

View File

@@ -4,7 +4,7 @@
VITE_BASE_URL= VITE_BASE_URL=
# API 地址前缀 # API 地址前缀
VITE_API_URL=https://cmp-api.boss160.cn VITE_API_URL=https://cmp-api.xm-iot.cn
# Delete console # Delete console
VITE_DROP_CONSOLE=true VITE_DROP_CONSOLE=true

View File

@@ -25,7 +25,7 @@ RUN pnpm install --frozen-lockfile
COPY . . COPY . .
# 构建生产版本(跳过类型检查,直接构建) # 构建生产版本(跳过类型检查,直接构建)
RUN pnpm run no RUN pnpm run nd
# ================================ # ================================
# 阶段 2: 运行阶段 # 阶段 2: 运行阶段

View File

@@ -9,12 +9,14 @@
### 1. 套餐管理模块 (Package Management) ✅ ### 1. 套餐管理模块 (Package Management) ✅
- [x] **套餐系列页面** (`/package-management/package-series`) - [x] **套餐系列页面** (`/package-management/package-series`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增套餐系列"按钮添加 `v-permission="'package_series:add'"` - ✅ 为"新增套餐系列"按钮添加 `v-permission="'package_series:add'"`
- ✅ 为状态切换添加 `disabled: !hasAuth('package_series:update_status')` - ✅ 为状态切换添加 `disabled: !hasAuth('package_series:update_status')`
- ✅ 在操作列中使用 `hasAuth()` 判断编辑权限 (`package_series:edit`)和删除权限 (`package_series:delete`) - ✅ 在操作列中使用 `hasAuth()` 判断编辑权限 (`package_series:edit`)和删除权限 (`package_series:delete`)
- [x] **套餐列表页面** (`/package-management/package-list`) - [x] **套餐列表页面** (`/package-management/package-list`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增套餐"按钮添加 `v-permission="'package:add'"` - ✅ 为"新增套餐"按钮添加 `v-permission="'package:add'"`
- ✅ 为上/下架切换添加 `disabled: !hasAuth('package:update_away')` - ✅ 为上/下架切换添加 `disabled: !hasAuth('package:update_away')`
@@ -22,6 +24,7 @@
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`package:edit`) 和删除 (`package:delete`) 权限 - ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`package:edit`) 和删除 (`package:delete`) 权限
- [x] **单套餐分配页面** (`/package-management/package-assign`) - [x] **单套餐分配页面** (`/package-management/package-assign`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增分配"按钮添加 `v-permission="'package_assign:add'"` - ✅ 为"新增分配"按钮添加 `v-permission="'package_assign:add'"`
- ✅ 为状态切换添加 `disabled: !hasAuth('package_assign:update_status')` - ✅ 为状态切换添加 `disabled: !hasAuth('package_assign:update_status')`
@@ -60,6 +63,7 @@
### 4. 资产管理模块 (Asset Management) ✅ ### 4. 资产管理模块 (Asset Management) ✅
- [x] **IoT卡管理页面** (`/asset-management/iot-card-management`) - [x] **IoT卡管理页面** (`/asset-management/iot-card-management`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为表格头部操作按钮添加权限控制: - ✅ 为表格头部操作按钮添加权限控制:
- 批量分配: `v-permission="'iot_card:batch_allocation'"` - 批量分配: `v-permission="'iot_card:batch_allocation'"`
@@ -73,16 +77,19 @@
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **IoT卡任务页面** (`/asset-management/iot-card-task`) - [x] **IoT卡任务页面** (`/asset-management/iot-card-task`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"批量导入IoT卡"按钮添加 `v-permission="''"` - ✅ 为"批量导入IoT卡"按钮添加 `v-permission="''"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **设备任务页面** (`/asset-management/device-task`) - [x] **设备任务页面** (`/asset-management/device-task`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"批量导入设备"按钮添加 `v-permission="'device_task:bulk_import'"` - ✅ 为"批量导入设备"按钮添加 `v-permission="'device_task:bulk_import'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **设备管理页面** (`/asset-management/device-list`) - [x] **设备管理页面** (`/asset-management/device-list`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为表格头部操作按钮添加权限控制: - ✅ 为表格头部操作按钮添加权限控制:
- 批量分配: `v-permission="'devices:batch_allocation'"` - 批量分配: `v-permission="'devices:batch_allocation'"`
@@ -101,6 +108,7 @@
### 5. 账户管理模块 (Account Module) ✅ ### 5. 账户管理模块 (Account Module) ✅
- [x] **企业客户页面** (`/account/enterprise-customer`) - [x] **企业客户页面** (`/account/enterprise-customer`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增企业客户"按钮添加 `v-permission="'enterprise_customer:add'"` - ✅ 为"新增企业客户"按钮添加 `v-permission="'enterprise_customer:add'"`
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限: - ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
@@ -111,6 +119,7 @@
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **运营商管理页面** (`/account/carrier-management`) - [x] **运营商管理页面** (`/account/carrier-management`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增运营商"按钮添加 `v-permission="'carrier:add'"` - ✅ 为"新增运营商"按钮添加 `v-permission="'carrier:add'"`
- ✅ 为状态切换添加 `disabled: !hasAuth('carrier:update_status')` - ✅ 为状态切换添加 `disabled: !hasAuth('carrier:update_status')`
@@ -118,16 +127,19 @@
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **订单管理页面** (`/account/orders`) - [x] **订单管理页面** (`/account/orders`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"创建订单"按钮添加 `v-permission="'orders:add'"` - ✅ 为"创建订单"按钮添加 `v-permission="'orders:add'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **提现配置页面** (`/account/commission/withdrawal-settings`) - [x] **提现配置页面** (`/account/commission/withdrawal-settings`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增配置"按钮添加 `v-permission="'withdrawal_settings:add'"` - ✅ 为"新增配置"按钮添加 `v-permission="'withdrawal_settings:add'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **我的佣金页面** (`/account/commission/my-commission`) - [x] **我的佣金页面** (`/account/commission/my-commission`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"发起提现"按钮添加 `v-permission="'my_commission:add'"` - ✅ 为"发起提现"按钮添加 `v-permission="'my_commission:add'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
@@ -148,6 +160,7 @@
### 7. 测试与文档 ### 7. 测试与文档
- [ ] **权限控制测试** - [ ] **权限控制测试**
- 创建不同权限组合的测试用户 - 创建不同权限组合的测试用户
- 验证每个页面的按钮显示/隐藏符合预期 - 验证每个页面的按钮显示/隐藏符合预期
- 验证状态切换的启用/禁用符合预期 - 验证状态切换的启用/禁用符合预期

View File

@@ -0,0 +1,45 @@
# Change: 更新批量套餐系列绑定接口契约
## Why
当前批量设置套餐系列的两个接口仍然主要按“显式已选列表”思路使用:
- `PATCH /api/admin/devices/series-binding`
- `PATCH /api/admin/iot-cards/series-binding`
但最新业务要求这两个接口的使用方式需要尽量对齐现有“批量分配”流程,支持按列表、范围、筛选条件三种方式选择目标资产,而不是只能依赖表格勾选后的 ID/ICCID 列表。
如果接口契约不统一,前端无法复用批量分配/批量回收的选择模型,设备管理和单卡管理也会继续保留两套不同的批量操作交互与参数拼装逻辑。
## What Changes
- 为设备批量套餐系列绑定接口补充与批量操作一致的目标选择模型:
- 支持 `selection_type=list|range|filter`
- 支持 `device_ids``virtual_no_start` / `virtual_no_end`、以及设备筛选条件三类入参
- 在设备筛选条件中的“店铺名称”选择器复用 `GET /api/admin/shops/cascade?exclude_self=true` 级联店铺查询接口,并按最后一级节点提交 `shop_id`
- 保留 `series_id=0` 作为清除套餐系列绑定的语义
- 保留旧版兼容:未传 `selection_type` 时按 `device_ids` 处理
- 为单卡批量套餐系列绑定接口补充与单卡批量分配/回收一致的目标选择模型:
- 支持 `selection_type=list|range|filter`
- 支持 `iccids``iccid_start` / `iccid_end`、以及单卡筛选条件三类入参
- 保留 `series_id=0` 作为清除套餐系列绑定的语义
- 保留旧版兼容:未传 `selection_type` 时按 `iccids` 处理
- 两个接口均保持现有 PATCH 路径不变
- 两个接口继续返回批处理结果摘要与失败明细,不在本提案中引入新的异步任务机制或新的资源路径
## Impact
- Affected specs:
- `device-series-binding`
- `iot-card-series-binding`
- Affected code:
- `src/api/modules/device.ts`
- `src/api/modules/card.ts`
- `src/api/modules/shop.ts`
- `src/types/api/device.ts`
- `src/types/api/card.ts`
- `src/views/asset-management/device-list/index.vue`
- `src/views/asset-management/iot-card-management/index.vue`
- Reference docs:
- `docs/mvp/批量分配套餐系列.md`
- `docs/1-24下午-新功能.md`

View File

@@ -0,0 +1,81 @@
## ADDED Requirements
### Requirement: Device Batch Series Binding Selection Contract
The system SHALL expose `PATCH /api/admin/devices/series-binding` using a batch target-selection contract consistent with other batch asset operations.
The request MUST include `series_id`. The request MAY omit `selection_type` only for backward compatibility, and in that case the system MUST treat the request as `selection_type=list`.
When `selection_type=list`, the request MUST use `device_ids` to identify the target devices and MUST support up to 1000 device IDs.
When `selection_type=range`, the request MUST use `virtual_no_start` and `virtual_no_end` to identify the target device range.
When `selection_type=filter`, the request MUST support selecting the full filtered result set by the same device query dimensions used for batch operations, including `virtual_no`, `device_name`, `device_type`, `manufacturer`, `batch_no`, `status`, `shop_id`, `shop_ids`, `filter_series_id`, `created_at_start`, and `created_at_end`.
`series_id=0` MUST mean clearing the current series binding instead of binding a new series.
#### Scenario: Bind device series by explicit device ID list
- **GIVEN** 用户已经明确选中了多台设备
- **WHEN** 系统调用 `PATCH /api/admin/devices/series-binding`
- **AND** 请求体包含 `selection_type="list"``series_id=12``device_ids=[1001,1002,1003]`
- **THEN** 系统 MUST 按 `device_ids` 对这批设备设置目标套餐系列绑定
#### Scenario: Bind device series by virtual number range
- **GIVEN** 用户希望按设备虚拟号区间批量设置套餐系列
- **WHEN** 系统调用 `PATCH /api/admin/devices/series-binding`
- **AND** 请求体包含 `selection_type="range"``series_id=12``virtual_no_start="V000001"``virtual_no_end="V000100"`
- **THEN** 系统 MUST 按虚拟号范围选出目标设备并设置套餐系列绑定
#### Scenario: Clear device series binding by filter result set
- **GIVEN** 用户希望对满足筛选条件的全部设备清除当前套餐系列关联
- **WHEN** 系统调用 `PATCH /api/admin/devices/series-binding`
- **AND** 请求体包含 `selection_type="filter"``series_id=0``status=1``shop_ids=[1,2]`
- **THEN** 系统 MUST 对完整筛选结果集执行清除操作
- **AND** 系统 MUST NOT 只处理当前分页内已渲染的设备
#### Scenario: Omitted selection type remains compatible with legacy callers
- **GIVEN** 旧版调用方只提交 `device_ids``series_id`
- **WHEN** 系统调用 `PATCH /api/admin/devices/series-binding`
- **AND** 请求体包含 `device_ids=[1001,1002]``series_id=8`
- **THEN** 系统 MUST 将该请求视为 `selection_type="list"`
- **AND** 系统 MUST 继续按 `device_ids` 处理目标设备
### Requirement: Device Batch Series Binding Shop Filter Selector
The system SHALL use the cascade shop query endpoint for the shop selector in device batch series binding filter mode.
When the user filters devices by shop in the batch series binding dialog, the UI MUST load shop options from `GET /api/admin/shops/cascade` with `exclude_self=true`, present them as a cascade selector, and submit the last selected node as `shop_id`.
#### Scenario: Shop filter uses cascade endpoint with self excluded
- **GIVEN** 用户打开“批量设置设备套餐系列绑定”弹窗
- **AND** 用户选择 `selection_type="filter"`
- **WHEN** 用户展开“店铺名称”选择器
- **THEN** 系统 MUST 调用 `GET /api/admin/shops/cascade`
- **AND** 请求参数 MUST 包含 `exclude_self=true`
- **AND** 系统 MUST 按级联树结构展示可选店铺
#### Scenario: Shop cascade selection submits the last selected node
- **GIVEN** 用户在“店铺名称”级联选择器中依次选择某条店铺路径
- **WHEN** 用户确认提交设备批量套餐系列绑定
- **THEN** 系统 MUST 取该路径最后一级节点的 ID 作为请求中的 `shop_id`
### Requirement: Device Batch Series Binding Result Summary
The system SHALL return a batch processing summary for device series binding requests.
The result data MUST include `success_count`, `fail_count`, and `failed_items`. Each entry in `failed_items` MUST include `device_id`, `virtual_no`, and `reason`.
#### Scenario: Partial device binding failure returns itemized reasons
- **GIVEN** 某次设备批量套餐系列绑定同时存在成功项和失败项
- **WHEN** 接口返回处理结果
- **THEN** 响应数据 MUST 包含成功数量和失败数量
- **AND** 每条失败明细 MUST 返回对应的 `device_id`
- **AND** 每条失败明细 MUST 返回对应的 `virtual_no`
- **AND** 每条失败明细 MUST 返回可展示的失败原因 `reason`

View File

@@ -0,0 +1,59 @@
## ADDED Requirements
### Requirement: IoT Card Batch Series Binding Selection Contract
The system SHALL expose `PATCH /api/admin/iot-cards/series-binding` using the same batch target-selection pattern as standalone card batch allocation and recall.
The request MUST include `series_id`. The request MAY omit `selection_type` only for backward compatibility, and in that case the system MUST treat the request as `selection_type=list`.
When `selection_type=list`, the request MUST use `iccids` to identify the target cards and MUST support up to 1000 ICCIDs.
When `selection_type=range`, the request MUST use `iccid_start` and `iccid_end` to identify the target ICCID range.
When `selection_type=filter`, the request MUST support the same primary filter dimensions used by standalone card batch allocation and recall, including `carrier_id`, `status`, and `batch_no`.
`series_id=0` MUST mean clearing the current series binding instead of binding a new series.
#### Scenario: Bind card series by ICCID list
- **GIVEN** 用户已经明确选中了多张单卡
- **WHEN** 系统调用 `PATCH /api/admin/iot-cards/series-binding`
- **AND** 请求体包含 `selection_type="list"``series_id=12``iccids=["8986000000000000001","8986000000000000002"]`
- **THEN** 系统 MUST 按 `iccids` 对这批卡设置目标套餐系列绑定
#### Scenario: Bind card series by ICCID range
- **GIVEN** 用户希望按 ICCID 号段批量设置套餐系列
- **WHEN** 系统调用 `PATCH /api/admin/iot-cards/series-binding`
- **AND** 请求体包含 `selection_type="range"``series_id=12``iccid_start="8986000000000000001"``iccid_end="8986000000000000100"`
- **THEN** 系统 MUST 按 ICCID 范围选出目标卡并设置套餐系列绑定
#### Scenario: Clear card series binding by standalone-card filters
- **GIVEN** 用户希望对满足单卡筛选条件的全部卡清除当前套餐系列关联
- **WHEN** 系统调用 `PATCH /api/admin/iot-cards/series-binding`
- **AND** 请求体包含 `selection_type="filter"``series_id=0``carrier_id=1``status=1`
- **THEN** 系统 MUST 对完整筛选结果集执行清除操作
- **AND** 系统 MUST NOT 只处理当前分页内已渲染的卡
#### Scenario: Omitted selection type remains compatible with legacy callers
- **GIVEN** 旧版调用方只提交 `iccids``series_id`
- **WHEN** 系统调用 `PATCH /api/admin/iot-cards/series-binding`
- **AND** 请求体包含 `iccids=["8986000000000000001"]``series_id=8`
- **THEN** 系统 MUST 将该请求视为 `selection_type="list"`
- **AND** 系统 MUST 继续按 `iccids` 处理目标卡
### Requirement: IoT Card Batch Series Binding Result Summary
The system SHALL return a batch processing summary for IoT card series binding requests.
The result data MUST include `success_count`, `fail_count`, and `failed_items`. Each entry in `failed_items` MUST include `iccid` and `reason`.
#### Scenario: Partial card binding failure returns itemized reasons
- **GIVEN** 某次单卡批量套餐系列绑定同时存在成功项和失败项
- **WHEN** 接口返回处理结果
- **THEN** 响应数据 MUST 包含成功数量和失败数量
- **AND** 每条失败明细 MUST 返回对应的 `iccid`
- **AND** 每条失败明细 MUST 返回可展示的失败原因 `reason`

View File

@@ -0,0 +1,8 @@
## 1. Implementation
- [x] 1.1 更新设备批量套餐系列绑定的请求类型与 API 方法签名,使其支持 `list` / `range` / `filter` 三种选择方式。
- [x] 1.2 更新单卡批量套餐系列绑定的请求类型与 API 方法签名,使其对齐单卡批量分配/回收的选择方式。
- [x] 1.3 调整设备管理页面的“批量设置套餐系列”交互,使其支持与批量操作一致的目标选择模型,并让店铺筛选复用 `shops/cascade?exclude_self=true` 级联接口。
- [x] 1.4 调整单卡管理页面的“批量设置套餐系列”交互,使其支持与批量分配/回收一致的目标选择模型。
- [ ] 1.5 验证 `series_id=0` 的批量清除和单条清除入口仍然可用。
- [ ] 1.6 验证未传 `selection_type` 的旧调用仍按列表模式兼容处理。

View File

@@ -25,12 +25,15 @@
## Decisions ## Decisions
- Decision: 在前端类型层面显式补齐新增字段,而不是通过 `any` 或临时属性兼容 - Decision: 在前端类型层面显式补齐新增字段,而不是通过 `any` 或临时属性兼容
- Rationale: 这次变化的核心是语义纠偏。如果类型层继续缺字段,页面很容易回退到旧假设。 - Rationale: 这次变化的核心是语义纠偏。如果类型层继续缺字段,页面很容易回退到旧假设。
- Decision: 列表页必须同时呈现佣金流程状态和佣金业务结果,但不强制唯一 UI 形式 - Decision: 列表页必须同时呈现佣金流程状态和佣金业务结果,但不强制唯一 UI 形式
- Rationale: `commission_status_name` 仅代表流程状态,`commission_result_name` 代表业务结果。无论是两列展示,还是“主状态 + 副标签”,都必须让用户能明确区分这两个维度。 - Rationale: `commission_status_name` 仅代表流程状态,`commission_result_name` 代表业务结果。无论是两列展示,还是“主状态 + 副标签”,都必须让用户能明确区分这两个维度。
- Decision: 详情页将“操作者信息”和“业务归属信息”拆成两组展示项 - Decision: 详情页将“操作者信息”和“业务归属信息”拆成两组展示项
- Rationale: 同一信息块内混放会继续强化旧认知,不利于用户理解订单责任归属。 - Rationale: 同一信息块内混放会继续强化旧认知,不利于用户理解订单责任归属。
- Decision: 所有“是否有佣金”的判断必须同时参考 `commission_status``commission_result` - Decision: 所有“是否有佣金”的判断必须同时参考 `commission_status``commission_result`
@@ -39,6 +42,7 @@
## Risks / Trade-offs ## Risks / Trade-offs
- 风险: 订单模块以外可能也有复用订单类型或字段语义的代码。 - 风险: 订单模块以外可能也有复用订单类型或字段语义的代码。
- Mitigation: 实施前使用全局搜索盘点 `operator_name``commission_status``commission_result` 的相关用法。 - Mitigation: 实施前使用全局搜索盘点 `operator_name``commission_status``commission_result` 的相关用法。
- 风险: 列表新增列后表格宽度会增加。 - 风险: 列表新增列后表格宽度会增加。

View File

@@ -22,15 +22,19 @@
## Decisions ## Decisions
- Decision: 为订单创建弹窗引入独立的“凭证上传中”状态,而不是复用 `createLoading` - Decision: 为订单创建弹窗引入独立的“凭证上传中”状态,而不是复用 `createLoading`
- Rationale: `createLoading` 表示“正在创建订单”,`voucherUploading` 表示“正在上传图片”,两者语义不同,混用会导致按钮文案和禁用逻辑失真。 - Rationale: `createLoading` 表示“正在创建订单”,`voucherUploading` 表示“正在上传图片”,两者语义不同,混用会导致按钮文案和禁用逻辑失真。
- Decision: 切卡模式弹窗改为同时收集 `iot_card_id``switch_mode` - Decision: 切卡模式弹窗改为同时收集 `iot_card_id``switch_mode`
- Rationale: 最新接口契约要求请求体必须包含这两个字段,前端需要在提交前把选择结果显式建模。 - Rationale: 最新接口契约要求请求体必须包含这两个字段,前端需要在提交前把选择结果显式建模。
- Decision: 设备未配置 IMEI 或没有任何绑定卡时,前端直接阻止提交 - Decision: 设备未配置 IMEI 或没有任何绑定卡时,前端直接阻止提交
- Rationale: 这是当前确认保留的前置条件,应该在界面层尽早反馈,而不是把失败留到接口返回后再处理。 - Rationale: 这是当前确认保留的前置条件,应该在界面层尽早反馈,而不是把失败留到接口返回后再处理。
- Decision: 设备列表页和资产信息页共用同一套切卡模式弹窗提交契约 - Decision: 设备列表页和资产信息页共用同一套切卡模式弹窗提交契约
- Rationale: 两个入口都在执行相同的业务动作,统一参数结构和校验逻辑可以避免行为漂移。 - Rationale: 两个入口都在执行相同的业务动作,统一参数结构和校验逻辑可以避免行为漂移。
- Decision: 只移除设备实时信息里的 `IP 地址` 字段,不影响 `WAN IP``LAN IP` - Decision: 只移除设备实时信息里的 `IP 地址` 字段,不影响 `WAN IP``LAN IP`

View File

@@ -42,5 +42,5 @@
- **AND** 当前套餐零售价上限为 `40.00` - **AND** 当前套餐零售价上限为 `40.00`
- **WHEN** 用户输入大于 `40.00` 的零售价并提交 - **WHEN** 用户输入大于 `40.00` 的零售价并提交
- **THEN** 系统阻止提交 - **THEN** 系统阻止提交
- **AND** 系统显示“零售价不能大于建议售价 * 2”对应的校验提示 - **AND** 系统显示“零售价不能大于建议售价 \* 2”对应的校验提示
- **AND** 系统不得调用修改零售价接口 - **AND** 系统不得调用修改零售价接口

View File

@@ -3,9 +3,16 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open --mode development",
"build": "vue-tsc --noEmit && vite build", "dev:default": "vite --open --mode default",
"no": "vite build", "dev:development": "vite --open --mode development",
"dev:production": "vite --open --mode production",
"build": "vue-tsc --noEmit && vite build --mode production",
"build:default": "vue-tsc --noEmit && vite build --mode default",
"build:development": "vue-tsc --noEmit && vite build --mode development",
"build:production": "vue-tsc --noEmit && vite build --mode production",
"np": "vite build --mode production",
"nd": "vite build --mode development",
"serve": "vite preview", "serve": "vite preview",
"lint": "eslint", "lint": "eslint",
"fix": "eslint --fix", "fix": "eslint --fix",

View File

@@ -28,7 +28,9 @@ export class AccountService extends BaseService {
* POST /api/admin/accounts * POST /api/admin/accounts
* @param data 账号数据 * @param data 账号数据
*/ */
static createAccount(data: CreatePlatformAccountParams): Promise<BaseResponse<{ ID?: number; id?: number }>> { static createAccount(
data: CreatePlatformAccountParams
): Promise<BaseResponse<{ ID?: number; id?: number }>> {
return this.create('/api/admin/accounts', data) return this.create('/api/admin/accounts', data)
} }

View File

@@ -23,7 +23,9 @@ import type {
ImportIotCardRequest, ImportIotCardRequest,
ImportIotCardResponse, ImportIotCardResponse,
IotCardImportTask, IotCardImportTask,
IotCardImportTaskDetail IotCardImportTaskDetail,
BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse
} from '@/types/api' } from '@/types/api'
export class CardService extends BaseService { export class CardService extends BaseService {
@@ -360,11 +362,13 @@ export class CardService extends BaseService {
* 批量设置卡的套餐系列绑定 * 批量设置卡的套餐系列绑定
* @param data 请求参数 * @param data 请求参数
*/ */
static batchSetCardSeriesBinding(data: { static batchSetCardSeriesBinding(
iccids: string[] data: BatchSetCardSeriesBindingRequest
series_id: number ): Promise<BaseResponse<BatchSetCardSeriesBindingResponse>> {
}): Promise<BaseResponse<any>> { return this.patch<BaseResponse<BatchSetCardSeriesBindingResponse>>(
return this.patch('/api/admin/iot-cards/series-binding', data) '/api/admin/iot-cards/series-binding',
data
)
} }
// ========== IoT卡网关操作相关 ========== // ========== IoT卡网关操作相关 ==========

View File

@@ -15,6 +15,8 @@ import type {
AllocateDevicesResponse, AllocateDevicesResponse,
RecallDevicesRequest, RecallDevicesRequest,
RecallDevicesResponse, RecallDevicesResponse,
BatchSetDeviceSeriesBindingRequest,
BatchSetDeviceSeriesBindingResponse,
ImportDeviceRequest, ImportDeviceRequest,
ImportDeviceResponse, ImportDeviceResponse,
DeviceImportTaskQueryParams, DeviceImportTaskQueryParams,
@@ -158,11 +160,13 @@ export class DeviceService extends BaseService {
* 批量设置设备的套餐系列绑定 * 批量设置设备的套餐系列绑定
* @param data 请求参数 * @param data 请求参数
*/ */
static batchSetDeviceSeriesBinding(data: { static batchSetDeviceSeriesBinding(
device_ids: number[] data: BatchSetDeviceSeriesBindingRequest
series_id: number ): Promise<BaseResponse<BatchSetDeviceSeriesBindingResponse>> {
}): Promise<BaseResponse<any>> { return this.patch<BaseResponse<BatchSetDeviceSeriesBindingResponse>>(
return this.patch('/api/admin/devices/series-binding', data) '/api/admin/devices/series-binding',
data
)
} }
// ========== 设备操作相关 ========== // ========== 设备操作相关 ==========

View File

@@ -32,6 +32,7 @@ export class ShopService extends BaseService {
static getShopsCascade(params?: { static getShopsCascade(params?: {
shop_name?: string shop_name?: string
parent_id?: number parent_id?: number
exclude_self?: boolean
}): Promise< }): Promise<
BaseResponse< BaseResponse<
Array<{ Array<{

View File

@@ -1,10 +1,5 @@
<template> <template>
<ElDialog <ElDialog v-model="dialogVisible" title="创建退款申请" width="30%" @closed="handleDialogClosed">
v-model="dialogVisible"
title="创建退款申请"
width="30%"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px"> <ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<ElFormItem label="订单号" prop="order_id"> <ElFormItem label="订单号" prop="order_id">
<ElSelect <ElSelect
@@ -79,7 +74,7 @@
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: boolean] 'update:modelValue': [value: boolean]
'success': [] success: []
}>() }>()
const dialogVisible = computed({ const dialogVisible = computed({

View File

@@ -42,11 +42,24 @@
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`" :label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
:value="card.iccid" :value="card.iccid"
> >
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%"> <div
style="
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
"
>
<span>{{ card.iccid }}</span> <span>{{ card.iccid }}</span>
<div> <div>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag> <ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
<ElTag v-if="card.carrier_name" size="small" type="success" style="margin-left: 5px">{{ card.carrier_name }}</ElTag> <ElTag
v-if="card.carrier_name"
size="small"
type="success"
style="margin-left: 5px"
>{{ card.carrier_name }}</ElTag
>
</div> </div>
</div> </div>
</ElOption> </ElOption>

View File

@@ -58,7 +58,7 @@ export function useLogin() {
formData.password = remembered.password formData.password = remembered.password
formData.rememberPassword = remembered.rememberPassword formData.rememberPassword = remembered.rememberPassword
} else { } else {
if (import.meta.env.MODE === 'development') { if (import.meta.env.DEV) {
formData.username = 'admin' formData.username = 'admin'
formData.password = 'Admin@123456' formData.password = 'Admin@123456'
formData.rememberPassword = false formData.rememberPassword = false

View File

@@ -14,7 +14,8 @@ export const router = createRouter({
// 处理路由错误Chunk 加载失败等) // 处理路由错误Chunk 加载失败等)
router.onError((error) => { router.onError((error) => {
const pattern = /Loading chunk (\d+) failed|Failed to fetch dynamically imported module|Couldn't resolve component/ const pattern =
/Loading chunk (\d+) failed|Failed to fetch dynamically imported module|Couldn't resolve component/
const isChunkLoadFailed = error.message && pattern.test(error.message) const isChunkLoadFailed = error.message && pattern.test(error.message)
if (isChunkLoadFailed) { if (isChunkLoadFailed) {

View File

@@ -527,8 +527,14 @@ export interface IotCardDetailResponse {
// 批量设置卡的套餐系列绑定请求参数 // 批量设置卡的套餐系列绑定请求参数
export interface BatchSetCardSeriesBindingRequest { export interface BatchSetCardSeriesBindingRequest {
iccids: string[] // ICCID列表最多500个 selection_type?: CardSelectionType // 选卡方式(兼容旧版时可不传,默认 list
series_id: number // 套餐系列ID0表示清除关联 series_id: number // 套餐系列ID0表示清除关联
iccids?: string[] // ICCID列表selection_type=list时必填最多1000个
iccid_start?: string // 起始ICCIDselection_type=range时必填
iccid_end?: string // 结束ICCIDselection_type=range时必填
carrier_id?: number // 运营商IDselection_type=filter时可选
status?: StandaloneCardStatus // 卡状态selection_type=filter时可选
batch_no?: string // 批次号selection_type=filter时可选
} }
// 卡套餐系列绑定失败项 // 卡套餐系列绑定失败项

View File

@@ -237,10 +237,31 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
// ========== 批量设置设备的套餐系列绑定相关 ========== // ========== 批量设置设备的套餐系列绑定相关 ==========
// 选设备方式枚举
export enum DeviceSelectionType {
LIST = 'list', // 设备ID列表
RANGE = 'range', // 设备号范围
FILTER = 'filter' // 筛选条件
}
// 批量设置设备的套餐系列绑定请求参数 // 批量设置设备的套餐系列绑定请求参数
export interface BatchSetDeviceSeriesBindingRequest { export interface BatchSetDeviceSeriesBindingRequest {
device_ids: number[] // 设备ID列表最多500个 selection_type?: DeviceSelectionType // 设备方式(兼容旧版时可不传,默认 list
series_id: number // 套餐系列ID0表示清除关联 series_id: number // 套餐系列ID0表示清除关联
device_ids?: number[] // 设备ID列表selection_type=list时必填最多1000个
virtual_no_start?: string // 起始设备虚拟号selection_type=range时必填
virtual_no_end?: string // 结束设备虚拟号selection_type=range时必填
virtual_no?: string // 设备虚拟号selection_type=filter时可选
device_name?: string // 设备名称selection_type=filter时可选
device_type?: string // 设备类型selection_type=filter时可选
manufacturer?: string // 制造商selection_type=filter时可选
batch_no?: string // 批次号selection_type=filter时可选
status?: DeviceStatus // 设备状态selection_type=filter时可选
shop_id?: number // 店铺IDselection_type=filter时可选
shop_ids?: number[] // 店铺ID列表selection_type=filter时可选
filter_series_id?: number // 当前绑定套餐系列IDselection_type=filter时可选
created_at_start?: string // 创建时间开始selection_type=filter时可选
created_at_end?: string // 创建时间结束selection_type=filter时可选
} }
// 设备套餐系列绑定失败项 // 设备套餐系列绑定失败项

View File

@@ -7,7 +7,7 @@
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const ElButton: typeof import('element-plus/es')['ElButton'] const ElButton: (typeof import('element-plus/es'))['ElButton']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']

View File

@@ -6,7 +6,7 @@ import type { RequestOptions, ErrorMessageMode } from '@/types/api'
const axiosInstance = axios.create({ const axiosInstance = axios.create({
timeout: 15000, // 请求超时时间(毫秒) timeout: 15000, // 请求超时时间(毫秒)
baseURL: import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_API_URL, // API地址开发环境使用代理,生产环境使用完整URL baseURL: import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL, // 开发服务器使用代理,其他模式使用完整URL
withCredentials: false, // 异步请求携带cookie withCredentials: false, // 异步请求携带cookie
transformRequest: [ transformRequest: [
(data, headers) => { (data, headers) => {
@@ -272,9 +272,10 @@ async function request<T = any>(config: ExtendedRequestConfig): Promise<T> {
if (axios.isAxiosError(e)) { if (axios.isAxiosError(e)) {
// 默认显示错误消息,除非明确设置为 'none' // 默认显示错误消息,除非明确设置为 'none'
const errorMode = config.requestOptions?.errorMessageMode || 'message' const errorMode = config.requestOptions?.errorMessageMode || 'message'
const show404Error = config.requestOptions?.show404Error !== undefined const show404Error =
? config.requestOptions.show404Error config.requestOptions?.show404Error !== undefined
: true ? config.requestOptions.show404Error
: true
handleErrorMessage(e, errorMode, show404Error) handleErrorMessage(e, errorMode, show404Error)
} }
return Promise.reject(e) return Promise.reject(e)

View File

@@ -33,7 +33,9 @@
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
<div v-if="hasCardInfo" class="operation-button-group"> <div v-if="hasCardInfo" class="operation-button-group">
<ElButton @click="handleRefresh" type="primary" :icon="Refresh" :disabled="refreshDisabled"> 刷新 </ElButton> <ElButton @click="handleRefresh" type="primary" :icon="Refresh" :disabled="refreshDisabled">
刷新
</ElButton>
</div> </div>
</div> </div>
</ElCard> </ElCard>

View File

@@ -166,7 +166,10 @@
<div class="flow-progress-bar" style="margin-top: 16px"> <div class="flow-progress-bar" style="margin-top: 16px">
<ElProgress <ElProgress
:percentage=" :percentage="
getUsageProgress(getDisplayedUsedMb(currentPackage), getDisplayedTotalMb(currentPackage)) getUsageProgress(
getDisplayedUsedMb(currentPackage),
getDisplayedTotalMb(currentPackage)
)
" "
:color=" :color="
getProgressColorByPercentage( getProgressColorByPercentage(

View File

@@ -145,23 +145,13 @@
<template v-else> <template v-else>
<div class="progress-text"> <div class="progress-text">
<span class="used" <span class="used"
>已使用: >已使用: {{ formatDataSize(getDisplayedUsedMb(scope.row)) }}</span
{{
formatDataSize(
getDisplayedUsedMb(scope.row)
)
}}</span
> >
<span class="total" <span class="total"
>总量: {{ formatDataSize(scope.row.real_total_mb || 0) }}</span >总量: {{ formatDataSize(scope.row.real_total_mb || 0) }}</span
> >
<span class="remaining" <span class="remaining"
>剩余: >剩余: {{ formatDataSize(getDisplayedRemainingMb(scope.row)) }}</span
{{
formatDataSize(
getDisplayedRemainingMb(scope.row)
)
}}</span
> >
</div> </div>
<ElProgress <ElProgress

View File

@@ -390,7 +390,6 @@
color: var(--el-text-color-primary, #374151); color: var(--el-text-color-primary, #374151);
} }
} }
} }
.pagination-wrapper { .pagination-wrapper {

View File

@@ -146,9 +146,7 @@
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:modelValue', value)
}) })
const currentModeValue = computed<0 | 1>(() => const currentModeValue = computed<0 | 1>(() => (Number(props.currentMode) === 1 ? 1 : 0))
Number(props.currentMode) === 1 ? 1 : 0
)
const availableCards = computed(() => props.cards || []) const availableCards = computed(() => props.cards || [])
@@ -157,7 +155,9 @@
const syncFormState = () => { const syncFormState = () => {
form.switch_mode = currentModeValue.value form.switch_mode = currentModeValue.value
const selectedStillExists = availableCards.value.some((card) => card.iot_card_id === form.iot_card_id) const selectedStillExists = availableCards.value.some(
(card) => card.iot_card_id === form.iot_card_id
)
if (!selectedStillExists) { if (!selectedStillExists) {
form.iot_card_id = availableCards.value[0]?.iot_card_id form.iot_card_id = availableCards.value[0]?.iot_card_id
} }

View File

@@ -36,7 +36,6 @@
<ElButton <ElButton
type="info" type="info"
@click="handleBatchSetSeries" @click="handleBatchSetSeries"
:disabled="!selectedDevices.length"
v-permission="'device:batch_set_series'" v-permission="'device:batch_set_series'"
> >
批量设置套餐系列 批量设置套餐系列
@@ -200,6 +199,7 @@
v-model="seriesBindingDialogVisible" v-model="seriesBindingDialogVisible"
title="批量设置设备套餐系列绑定" title="批量设置设备套餐系列绑定"
width="600px" width="600px"
@close="handleCloseSeriesBindingDialog"
> >
<ElForm <ElForm
ref="seriesBindingFormRef" ref="seriesBindingFormRef"
@@ -210,7 +210,167 @@
<ElFormItem label="已选设备数"> <ElFormItem label="已选设备数">
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> <span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span>
</ElFormItem> </ElFormItem>
<ElFormItem label="套餐系列" prop="series_id"> <ElFormItem label="选设备方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type">
<ElRadio label="list">设备ID列表</ElRadio>
<ElRadio label="range">设备号范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.LIST"
label="设备列表"
>
<div>已选择 {{ selectedDevices.length }} 台设备</div>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
label="起始设备号"
prop="virtual_no_start"
>
<ElInput
v-model="seriesBindingForm.virtual_no_start"
placeholder="请输入起始设备号"
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.RANGE"
label="结束设备号"
prop="virtual_no_end"
>
<ElInput v-model="seriesBindingForm.virtual_no_end" placeholder="请输入结束设备号" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备号"
>
<ElInput
v-model="seriesBindingForm.virtual_no"
placeholder="请输入设备号"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备名称"
>
<ElInput
v-model="seriesBindingForm.device_name"
placeholder="请输入设备名称"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="设备类型"
>
<ElInput
v-model="seriesBindingForm.device_type"
placeholder="请输入设备类型"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="制造商"
>
<ElInput
v-model="seriesBindingForm.manufacturer"
placeholder="请输入制造商"
clearable
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="状态"
>
<ElSelect
v-model="seriesBindingForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in searchBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="店铺名称"
>
<ElCascader
v-model="seriesBindingForm.shop_id_path"
:options="seriesBindingShopCascadeOptions"
:props="seriesBindingShopCascadeProps"
placeholder="请选择店铺名称"
clearable
filterable
style="width: 100%"
/>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="当前套餐系列"
>
<ElSelect
v-model="seriesBindingForm.filter_series_id"
placeholder="请选择或搜索当前套餐系列"
clearable
filterable
remote
:remote-method="searchSeries"
style="width: 100%"
>
<ElOption
v-for="series in searchSeriesOptions"
:key="series.id"
:label="series.series_name"
:value="series.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === DeviceSelectionType.FILTER"
label="创建设备时间"
>
<ElDatePicker
v-model="seriesBindingForm.created_at_range"
type="daterange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="目标套餐系列" prop="series_id">
<ElSelect <ElSelect
v-model="seriesBindingForm.series_id" v-model="seriesBindingForm.series_id"
placeholder="请选择或搜索套餐系列" placeholder="请选择或搜索套餐系列"
@@ -517,6 +677,7 @@
AssetService, AssetService,
CarrierService CarrierService
} from '@/api/modules' } from '@/api/modules'
import { DeviceSelectionType } from '@/types/api'
import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus' import { ElMessage, ElMessageBox, ElTag, ElIcon, ElRadioGroup, ElRadio } from 'element-plus'
import { Loading } from '@element-plus/icons-vue' import { Loading } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
@@ -527,6 +688,7 @@
DeviceStatus, DeviceStatus,
AllocateDevicesResponse, AllocateDevicesResponse,
RecallDevicesResponse, RecallDevicesResponse,
BatchSetDeviceSeriesBindingRequest,
BatchSetDeviceSeriesBindingResponse BatchSetDeviceSeriesBindingResponse
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
@@ -593,6 +755,38 @@
label: 'label', label: 'label',
children: 'children' children: 'children'
} }
const seriesBindingShopCascadeOptions = ref<any[]>([])
const seriesBindingShopCascadeProps = {
lazy: true,
checkStrictly: true,
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({
parent_id: parentId,
exclude_self: true
})
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载批量设置套餐系列店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
const allocateResult = ref<AllocateDevicesResponse | null>(null) const allocateResult = ref<AllocateDevicesResponse | null>(null)
const recallResult = ref<RecallDevicesResponse | null>(null) const recallResult = ref<RecallDevicesResponse | null>(null)
@@ -602,11 +796,53 @@
const seriesBindingFormRef = ref<FormInstance>() const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false) const seriesLoading = ref(false)
const packageSeriesList = ref<PackageSeriesResponse[]>([]) const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({ const createInitialSeriesBindingState = () => ({
series_id: undefined as number | undefined selection_type: DeviceSelectionType.LIST,
series_id: undefined as number | undefined,
virtual_no_start: '',
virtual_no_end: '',
virtual_no: '',
device_name: '',
device_type: '',
manufacturer: '',
batch_no: '',
status: undefined as DeviceStatus | undefined,
shop_id_path: undefined as number[] | undefined,
filter_series_id: undefined as number | undefined,
created_at_range: [] as string[],
created_at_start: '',
created_at_end: ''
}) })
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const seriesBindingRules = reactive<FormRules>({ const seriesBindingRules = reactive<FormRules>({
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }] selection_type: [{ required: true, message: '请选择选设备方式', trigger: 'change' }],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
virtual_no_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE && !value) {
callback(new Error('请输入起始设备号'))
} else {
callback()
}
},
trigger: 'blur'
}
],
virtual_no_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE && !value) {
callback(new Error('请输入结束设备号'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}) })
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null) const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
@@ -1427,6 +1663,7 @@
.then(async () => { .then(async () => {
try { try {
const res = await DeviceService.batchSetDeviceSeriesBinding({ const res = await DeviceService.batchSetDeviceSeriesBinding({
selection_type: DeviceSelectionType.LIST,
device_ids: [device.id], device_ids: [device.id],
series_id: 0 series_id: 0
}) })
@@ -1558,14 +1795,18 @@
// 批量设置套餐系列 // 批量设置套餐系列
const handleBatchSetSeries = async () => { const handleBatchSetSeries = async () => {
if (selectedDevices.value.length === 0) {
ElMessage.warning('请先选择要设置的设备')
return
}
seriesBindingForm.series_id = undefined
seriesBindingResult.value = null seriesBindingResult.value = null
await loadPackageSeriesList() Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
selection_type:
selectedDevices.value.length > 0 ? DeviceSelectionType.LIST : DeviceSelectionType.FILTER
})
await Promise.all([
loadPackageSeriesList(),
loadSearchBatchNoOptions(),
loadSeriesBindingShopCascadeOptions()
])
seriesBindingDialogVisible.value = true seriesBindingDialogVisible.value = true
seriesBindingFormRef.value?.clearValidate()
} }
// 加载套餐系列列表支持名称搜索默认20条 // 加载套餐系列列表支持名称搜索默认20条
@@ -1596,6 +1837,66 @@
await loadPackageSeriesList(query || undefined) await loadPackageSeriesList(query || undefined)
} }
const loadSeriesBindingShopCascadeOptions = async () => {
try {
const res = await ShopService.getShopsCascade({
parent_id: undefined,
exclude_self: true
})
if (res.code === 0) {
seriesBindingShopCascadeOptions.value = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('加载批量设置套餐系列根级店铺失败:', error)
}
}
const buildSeriesBindingRequest = (): BatchSetDeviceSeriesBindingRequest | null => {
const request: BatchSetDeviceSeriesBindingRequest = {
selection_type: seriesBindingForm.selection_type,
series_id: seriesBindingForm.series_id!
}
if (seriesBindingForm.selection_type === DeviceSelectionType.LIST) {
const deviceIds = selectedDevices.value.map((device) => device.id)
if (deviceIds.length === 0) {
ElMessage.warning('列表模式下请先选择要设置的设备')
return null
}
request.device_ids = deviceIds
return request
}
if (seriesBindingForm.selection_type === DeviceSelectionType.RANGE) {
request.virtual_no_start = seriesBindingForm.virtual_no_start
request.virtual_no_end = seriesBindingForm.virtual_no_end
return request
}
if (seriesBindingForm.virtual_no) request.virtual_no = seriesBindingForm.virtual_no
if (seriesBindingForm.device_name) request.device_name = seriesBindingForm.device_name
if (seriesBindingForm.device_type) request.device_type = seriesBindingForm.device_type
if (seriesBindingForm.manufacturer) request.manufacturer = seriesBindingForm.manufacturer
if (seriesBindingForm.batch_no) request.batch_no = seriesBindingForm.batch_no
if (seriesBindingForm.status !== undefined) request.status = seriesBindingForm.status
if (seriesBindingForm.shop_id_path?.length) {
request.shop_id = seriesBindingForm.shop_id_path[seriesBindingForm.shop_id_path.length - 1]
}
if (seriesBindingForm.filter_series_id !== undefined) {
request.filter_series_id = seriesBindingForm.filter_series_id
}
if (seriesBindingForm.created_at_range.length === 2) {
request.created_at_start = seriesBindingForm.created_at_range[0]
request.created_at_end = seriesBindingForm.created_at_range[1]
}
return request
}
// 确认设置套餐系列绑定 // 确认设置套餐系列绑定
const handleConfirmSeriesBinding = async () => { const handleConfirmSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return if (!seriesBindingFormRef.value) return
@@ -1604,9 +1905,9 @@
if (valid) { if (valid) {
seriesBindingLoading.value = true seriesBindingLoading.value = true
try { try {
const data = { const data = buildSeriesBindingRequest()
device_ids: selectedDevices.value.map((d) => d.id), if (!data) {
series_id: seriesBindingForm.series_id! return
} }
const res = await DeviceService.batchSetDeviceSeriesBinding(data) const res = await DeviceService.batchSetDeviceSeriesBinding(data)
if (res.code === 0) { if (res.code === 0) {
@@ -1634,6 +1935,7 @@
const handleCloseSeriesBindingDialog = () => { const handleCloseSeriesBindingDialog = () => {
seriesBindingDialogVisible.value = false seriesBindingDialogVisible.value = false
seriesBindingResult.value = null seriesBindingResult.value = null
Object.assign(seriesBindingForm, createInitialSeriesBindingState())
if (seriesBindingFormRef.value) { if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields() seriesBindingFormRef.value.resetFields()
} }

View File

@@ -36,7 +36,6 @@
</ElButton> </ElButton>
<ElButton <ElButton
type="primary" type="primary"
:disabled="selectedCards.length === 0"
@click="showSeriesBindingDialog" @click="showSeriesBindingDialog"
v-permission="'iot_card:batch_setting'" v-permission="'iot_card:batch_setting'"
> >
@@ -351,11 +350,97 @@
ref="seriesBindingFormRef" ref="seriesBindingFormRef"
:model="seriesBindingForm" :model="seriesBindingForm"
:rules="seriesBindingRules" :rules="seriesBindingRules"
label-width="120px" label-width="100"
> >
<ElFormItem label="已选择卡数"> <ElFormItem label="选卡方式" prop="selection_type">
<ElRadioGroup v-model="seriesBindingForm.selection_type">
<ElRadio label="list">ICCID列表</ElRadio>
<ElRadio label="range">号段范围</ElRadio>
<ElRadio label="filter">筛选条件</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.LIST"
label="ICCID列表"
>
<div>已选择 {{ selectedCards.length }} 张卡</div> <div>已选择 {{ selectedCards.length }} 张卡</div>
</ElFormItem> </ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="起始ICCID"
prop="iccid_start"
>
<ElInput v-model="seriesBindingForm.iccid_start" placeholder="请输入起始ICCID" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.RANGE"
label="结束ICCID"
prop="iccid_end"
>
<ElInput v-model="seriesBindingForm.iccid_end" placeholder="请输入结束ICCID" />
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="运营商"
>
<ElSelect
v-model="seriesBindingForm.carrier_id"
placeholder="请选择或搜索运营商"
clearable
filterable
remote
:remote-method="handleSearchAllocateCarrier"
:loading="allocateCarrierLoading"
style="width: 100%"
>
<ElOption
v-for="item in allocateCarrierOptions"
:key="item.id"
:label="item.carrier_name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="seriesBindingForm.selection_type === CardSelectionType.FILTER"
label="卡状态"
>
<ElSelect
v-model="seriesBindingForm.status"
placeholder="请选择状态"
clearable
style="width: 100%"
>
<ElOption label="在库" :value="1" />
<ElOption label="已分销" :value="2" />
<ElOption label="已激活" :value="3" />
<ElOption label="已停用" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="
seriesBindingForm.selection_type === CardSelectionType.FILTER &&
userInfo.user_type !== 3
"
label="批次号"
>
<ElSelect
v-model="seriesBindingForm.batch_no"
placeholder="请选择或搜索批次号"
clearable
filterable
remote
:remote-method="handleSearchAllocateBatchNo"
style="width: 100%"
>
<ElOption
v-for="item in allocateBatchNoOptions"
:key="item.id"
:label="item.batch_no"
:value="item.batch_no"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="套餐系列" prop="series_id"> <ElFormItem label="套餐系列" prop="series_id">
<ElSelect <ElSelect
v-model="seriesBindingForm.series_id" v-model="seriesBindingForm.series_id"
@@ -550,6 +635,7 @@
AllocateStandaloneCardsRequest, AllocateStandaloneCardsRequest,
RecallStandaloneCardsRequest, RecallStandaloneCardsRequest,
AllocateStandaloneCardsResponse, AllocateStandaloneCardsResponse,
BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse BatchSetCardSeriesBindingResponse
} from '@/types/api/card' } from '@/types/api/card'
import { CardSelectionType } from '@/types/api/card' import { CardSelectionType } from '@/types/api/card'
@@ -593,11 +679,45 @@
const seriesBindingFormRef = ref<FormInstance>() const seriesBindingFormRef = ref<FormInstance>()
const seriesLoading = ref(false) const seriesLoading = ref(false)
const packageSeriesList = ref<PackageSeriesResponse[]>([]) const packageSeriesList = ref<PackageSeriesResponse[]>([])
const seriesBindingForm = reactive({ const createInitialSeriesBindingState = () => ({
series_id: undefined as number | undefined selection_type: CardSelectionType.LIST,
series_id: undefined as number | undefined,
iccid_start: '',
iccid_end: '',
carrier_id: undefined as number | undefined,
status: undefined as StandaloneCardStatus | undefined,
batch_no: ''
}) })
const seriesBindingForm = reactive(createInitialSeriesBindingState())
const seriesBindingRules = reactive<FormRules>({ const seriesBindingRules = reactive<FormRules>({
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }] selection_type: [{ required: true, message: '请选择选卡方式', trigger: 'change' }],
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
iccid_start: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入起始ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
],
iccid_end: [
{
required: true,
validator: (_rule, value, callback) => {
if (seriesBindingForm.selection_type === CardSelectionType.RANGE && !value) {
callback(new Error('请输入结束ICCID'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}) })
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({ const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
success_count: 0, success_count: 0,
@@ -1127,7 +1247,6 @@
{ label: '最后实名检查时间', prop: 'last_real_name_check_at' }, { label: '最后实名检查时间', prop: 'last_real_name_check_at' },
{ label: '启用轮询', prop: 'enable_polling' }, { label: '启用轮询', prop: 'enable_polling' },
{ label: '批次号', prop: 'batch_no' }, { label: '批次号', prop: 'batch_no' },
{ label: '供应商', prop: 'supplier' },
{ label: '创建时间', prop: 'created_at' }, { label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' } { label: '更新时间', prop: 'updated_at' }
] ]
@@ -1274,12 +1393,14 @@
prop: 'shop_name', prop: 'shop_name',
label: '店铺名称', label: '店铺名称',
minWidth: 150, minWidth: 150,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.shop_name || '-' formatter: (row: StandaloneIotCard) => row.shop_name || '-'
}, },
{ {
prop: 'series_name', prop: 'series_name',
label: '套餐系列', label: '套餐系列',
width: 150, width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.series_name || '-' formatter: (row: StandaloneIotCard) => row.series_name || '-'
}, },
{ {
@@ -1393,12 +1514,6 @@
width: 200, width: 200,
showOverflowTooltip: true showOverflowTooltip: true
}, },
{
prop: 'supplier',
label: '供应商',
width: 150,
formatter: (row: StandaloneIotCard) => row.supplier || '-'
},
{ {
prop: 'created_at', prop: 'created_at',
label: '创建时间', label: '创建时间',
@@ -1707,19 +1822,17 @@
// 显示套餐系列绑定对话框 // 显示套餐系列绑定对话框
const showSeriesBindingDialog = async () => { const showSeriesBindingDialog = async () => {
if (selectedCards.value.length === 0) { Object.assign(seriesBindingForm, createInitialSeriesBindingState(), {
ElMessage.warning('请先选择要设置的卡') selection_type:
return selectedCards.value.length > 0 ? CardSelectionType.LIST : CardSelectionType.FILTER
} })
await Promise.all([
// 加载套餐系列列表 loadPackageSeriesList(),
await loadPackageSeriesList() loadAllocateCarrierOptions(),
loadAllocateBatchNoOptions()
])
seriesBindingDialogVisible.value = true seriesBindingDialogVisible.value = true
seriesBindingForm.series_id = undefined seriesBindingFormRef.value?.clearValidate()
if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields()
}
} }
// 加载套餐系列列表支持名称搜索默认20条 // 加载套餐系列列表支持名称搜索默认20条
@@ -1752,6 +1865,7 @@
// 关闭套餐系列绑定对话框 // 关闭套餐系列绑定对话框
const handleSeriesBindingDialogClose = () => { const handleSeriesBindingDialogClose = () => {
Object.assign(seriesBindingForm, createInitialSeriesBindingState())
if (seriesBindingFormRef.value) { if (seriesBindingFormRef.value) {
seriesBindingFormRef.value.resetFields() seriesBindingFormRef.value.resetFields()
} }
@@ -1769,19 +1883,30 @@
await seriesBindingFormRef.value.validate(async (valid) => { await seriesBindingFormRef.value.validate(async (valid) => {
if (valid) { if (valid) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('请先选择要设置的卡')
return
}
seriesBindingLoading.value = true seriesBindingLoading.value = true
try { try {
const res = await CardService.batchSetCardSeriesBinding({ const request: BatchSetCardSeriesBindingRequest = {
iccids, selection_type: seriesBindingForm.selection_type,
series_id: seriesBindingForm.series_id! series_id: seriesBindingForm.series_id!
}) }
if (seriesBindingForm.selection_type === CardSelectionType.LIST) {
const iccids = selectedCards.value.map((card) => card.iccid)
if (iccids.length === 0) {
ElMessage.warning('列表模式下请先选择要设置的卡')
return
}
request.iccids = iccids
} else if (seriesBindingForm.selection_type === CardSelectionType.RANGE) {
request.iccid_start = seriesBindingForm.iccid_start
request.iccid_end = seriesBindingForm.iccid_end
} else {
if (seriesBindingForm.carrier_id) request.carrier_id = seriesBindingForm.carrier_id
if (seriesBindingForm.status) request.status = seriesBindingForm.status
if (seriesBindingForm.batch_no) request.batch_no = seriesBindingForm.batch_no
}
const res = await CardService.batchSetCardSeriesBinding(request)
if (res.code === 0) { if (res.code === 0) {
seriesBindingResult.value = res.data seriesBindingResult.value = res.data
@@ -1833,6 +1958,7 @@
.then(async () => { .then(async () => {
try { try {
const res = await CardService.batchSetCardSeriesBinding({ const res = await CardService.batchSetCardSeriesBinding({
selection_type: CardSelectionType.LIST,
iccids: selectedCards.value.map((card) => card.iccid), iccids: selectedCards.value.map((card) => card.iccid),
series_id: 0 series_id: 0
}) })
@@ -1885,6 +2011,7 @@
.then(async () => { .then(async () => {
try { try {
const res = await CardService.batchSetCardSeriesBinding({ const res = await CardService.batchSetCardSeriesBinding({
selection_type: CardSelectionType.LIST,
iccids: [iccid], iccids: [iccid],
series_id: 0 series_id: 0
}) })

View File

@@ -126,8 +126,16 @@
if (!data.card_category) return h('span', '-') if (!data.card_category) return h('span', '-')
return h( return h(
ElTag, ElTag,
{ type: data.card_category === 'industry' ? 'warning' : 'success', size: 'small' }, {
() => (data.card_category === 'normal' ? '普通卡' : data.card_category === 'industry' ? '行业卡' : data.card_category) type: data.card_category === 'industry' ? 'warning' : 'success',
size: 'small'
},
() =>
data.card_category === 'normal'
? '普通卡'
: data.card_category === 'industry'
? '行业卡'
: data.card_category
) )
} }
} }

View File

@@ -456,7 +456,8 @@
if (entityName.value || !accountList.value.length) return if (entityName.value || !accountList.value.length) return
const firstAccount = accountList.value[0] const firstAccount = accountList.value[0]
if (firstAccount) { if (firstAccount) {
entityName.value = (firstAccount as any).shop_name || (firstAccount as any).enterprise_name || '' entityName.value =
(firstAccount as any).shop_name || (firstAccount as any).enterprise_name || ''
} }
} }

View File

@@ -123,10 +123,7 @@
} }
// 获取佣金流程状态文本 // 获取佣金流程状态文本
const getCommissionStatusText = ( const getCommissionStatusText = (status?: OrderCommissionStatus, statusName?: string): string => {
status?: OrderCommissionStatus,
statusName?: string
): string => {
if (statusName) return statusName if (statusName) return statusName
if (status === undefined) return '-' if (status === undefined) return '-'
@@ -139,10 +136,7 @@
} }
// 获取佣金业务结果文本 // 获取佣金业务结果文本
const getCommissionResultText = ( const getCommissionResultText = (result?: OrderCommissionResult, resultName?: string): string => {
result?: OrderCommissionResult,
resultName?: string
): string => {
if (resultName) return resultName if (resultName) return resultName
if (result === undefined) return '-' if (result === undefined) return '-'

View File

@@ -463,7 +463,9 @@
if (value > retailPriceUpperLimit.value) { if (value > retailPriceUpperLimit.value) {
callback( callback(
new Error(`零售价不能大于建议售价 * 2 = ${formatRetailPriceYuan(retailPriceUpperLimit.value)}`) new Error(
`零售价不能大于建议售价 * 2 = ${formatRetailPriceYuan(retailPriceUpperLimit.value)}`
)
) )
return return
} }

View File

@@ -263,8 +263,9 @@
placeholder="请输入成本价" placeholder="请输入成本价"
/> />
<div v-if="packageForm.original_cost_price" class="form-tip"> <div v-if="packageForm.original_cost_price" class="form-tip">
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: 请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
¥{{ packageForm.original_cost_price.toFixed(2) }} packageForm.original_cost_price.toFixed(2)
}}
</div> </div>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>

View File

@@ -1992,5 +1992,4 @@
font-weight: 500; font-weight: 500;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }
</style> </style>

View File

@@ -107,8 +107,9 @@
placeholder="请输入成本价" placeholder="请输入成本价"
/> />
<div v-if="packageForm.original_cost_price" class="form-tip"> <div v-if="packageForm.original_cost_price" class="form-tip">
请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: 请参考{{ packageForm.package_name || '套餐' }} - 套餐成本价: ¥{{
¥{{ packageForm.original_cost_price.toFixed(2) }} packageForm.original_cost_price.toFixed(2)
}}
</div> </div>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
@@ -202,7 +203,8 @@
? pkgOption.cost_price / 100 ? pkgOption.cost_price / 100
: currentCostPrice, : currentCostPrice,
suggested_retail_price: suggested_retail_price:
pkgOption?.suggested_retail_price !== undefined && pkgOption?.suggested_retail_price !== null pkgOption?.suggested_retail_price !== undefined &&
pkgOption?.suggested_retail_price !== null
? pkgOption.suggested_retail_price / 100 ? pkgOption.suggested_retail_price / 100
: undefined : undefined
} }

View File

@@ -794,7 +794,12 @@
const isMaskedData = (value: string | undefined | null): boolean => { const isMaskedData = (value: string | undefined | null): boolean => {
if (!value) return false if (!value) return false
// 检查是否包含脱敏标记:纯星号、已配置标记、或包含连续星号(脱敏数据如Wuha***1218) // 检查是否包含脱敏标记:纯星号、已配置标记、或包含连续星号(脱敏数据如Wuha***1218)
return value === '***' || value.includes('[已配置]') || value.includes('[未配置]') || /\*{2,}/.test(value) return (
value === '***' ||
value.includes('[已配置]') ||
value.includes('[未配置]') ||
/\*{2,}/.test(value)
)
} }
// 显示编辑对话框 // 显示编辑对话框
@@ -906,7 +911,8 @@
description: form.description || undefined description: form.description || undefined
} }
// 只有填写了有效的新值才更新敏感字段(不是空值且不是脱敏数据) // 只有填写了有效的新值才更新敏感字段(不是空值且不是脱敏数据)
if (isValidFieldValue(form.miniapp_app_secret)) data.miniapp_app_secret = form.miniapp_app_secret if (isValidFieldValue(form.miniapp_app_secret))
data.miniapp_app_secret = form.miniapp_app_secret
if (isValidFieldValue(form.oa_app_secret)) data.oa_app_secret = form.oa_app_secret if (isValidFieldValue(form.oa_app_secret)) data.oa_app_secret = form.oa_app_secret
if (isValidFieldValue(form.oa_token)) data.oa_token = form.oa_token if (isValidFieldValue(form.oa_token)) data.oa_token = form.oa_token
if (isValidFieldValue(form.oa_aes_key)) data.oa_aes_key = form.oa_aes_key if (isValidFieldValue(form.oa_aes_key)) data.oa_aes_key = form.oa_aes_key