This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Change: 新增资产钱包自动续费配置(后台查询/保存 + 自动续费失败通知适配)
|
||||
|
||||
## Why
|
||||
|
||||
根据 `docs/产品迭代8月份/资产钱包.md`。后端测试环境(`https://cmp-api.boss160.cn`,`Authorization: Bearer <token>`)已提供资产钱包自动续费全局配置能力,仅有 2 个后台接口,H5 侧零新增:
|
||||
|
||||
- 查询资产钱包自动续费配置:`GET /api/admin/asset-auto-renewal-config`
|
||||
- 保存资产钱包自动续费配置:`PUT /api/admin/asset-auto-renewal-config`
|
||||
|
||||
后台需要提供配置页面承载总开关、适用范围、指定主套餐、到期前天数等配置;同时新增通知类型 `asset.auto_renewal.failed` 会复用既有通知接口产生新数据,需要适配展示与跳转。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增能力 `asset-wallet-auto-renewal`:
|
||||
- 配置读取:`GET /api/admin/asset-auto-renewal-config`,返回唯一一份全局配置的 `enabled`+`enabled_name`、`scope`+`scope_name`、`package_ids`、`days_before_expiry`、`config_version`、`updater`、`updated_at`。
|
||||
- 配置保存:`PUT /api/admin/asset-auto-renewal-config`:
|
||||
- 请求体 `{ enabled(0/1,必填), scope(all|specified,必填), package_ids(uint[],仅 specified 时非空且只能选当前可售主套餐), days_before_expiry(1–90,必填) }`。
|
||||
- 语义:单行配置、无新增/删除;保存即递增 `config_version`,记录操作者与前后值快照;配置变更只影响后续扫描,历史记录不重算。
|
||||
- 权限:仅超级管理员与平台账号可访问;其他身份(代理/企业/个人客户)`403`,提示「无权限操作该资源或资源不存在」,无权限与不存在不区分。
|
||||
- 通知适配(复用既有接口,不算新接口):
|
||||
- 后台站内通知:新增通知类型 `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`),接收人是业务员/店铺账号。走既有 `GET /api/admin/notifications` + `GET /api/admin/notifications/{id}/target`;确认 `target_type` 的 `iot_card` / `device` 在导航白名单;`available=false` 时只显示正文不跳转;计入未读数。
|
||||
- H5 个人客户通知:同一类型已由后端加入客户可见白名单,出现在既有 `GET /api/c/v1/notifications` 与 `unread-count`,客户侧只有文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面,本仓库无需改动。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
|
||||
- 明确不做(避免前端空等):无自动续费记录页/异常记录页接口、无尝试记录查询接口、无「有余额就不停机」开关、无 H5 客户侧新接口。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `asset-wallet-auto-renewal` — 新增能力
|
||||
- Affected code:
|
||||
- `src/types/api/assetWallet.ts`(新增)
|
||||
- `src/types/api/index.ts`
|
||||
- `src/api/modules/assetWallet.ts`(新增)
|
||||
- `src/api/modules/index.ts`
|
||||
- `src/views/settings/asset-wallet-auto-renewal/index.vue`(新增)
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/utils/business/notificationNavigation.ts`(确认/补充 `iot_card`、`device` 白名单)
|
||||
- `src/components/core/layouts/art-notification/index.vue`(确认 `expiry` 分类覆盖新类型)
|
||||
- `src/locales/langs/zh.json`、`src/locales/langs/en.json`
|
||||
@@ -0,0 +1,94 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 自动续费配置查询
|
||||
|
||||
The admin frontend SHALL load the single global asset wallet auto-renewal configuration through `GET /api/admin/asset-auto-renewal-config`. 响应字段 SHALL 包含并展示 `enabled`+`enabled_name`、`scope`+`scope_name`、`package_ids`、`days_before_expiry`、`config_version`、`updater`、`updated_at`。
|
||||
|
||||
#### Scenario: 读取全局自动续费配置
|
||||
|
||||
- **WHEN** 用户进入资产钱包自动续费配置页面
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/asset-auto-renewal-config`
|
||||
- **AND** 页面 MUST 展示总开关及中文名称、适用范围及中文名称、指定主套餐集合、统一到期前天数、配置版本、最近保存操作者与最近保存时间
|
||||
|
||||
### Requirement: 自动续费配置保存
|
||||
|
||||
The admin frontend SHALL save the configuration through `PUT /api/admin/asset-auto-renewal-config` with request body `{ enabled, scope, package_ids, days_before_expiry }`。`enabled` SHALL 取值 0/1 且必填;`scope` SHALL 取值 `all`(全部主套餐)或 `specified`(指定主套餐)且必填;`package_ids` 仅在 `scope=specified` 时必填非空且只能选择当前可售主套餐;`days_before_expiry` SHALL 取值 1 至 90 且必填。配置为单行、无新增/删除;保存 SHALL 递增 `config_version` 并记录操作者与前后值快照;配置变更 SHALL 只影响后续扫描,历史记录不重算。
|
||||
|
||||
#### Scenario: 保存全部主套餐范围配置
|
||||
|
||||
- **WHEN** 用户设置适用范围为「全部主套餐」并提交
|
||||
- **THEN** 前端 MUST 调用 `PUT /api/admin/asset-auto-renewal-config`
|
||||
- **AND** 请求体携带 `enabled`、`scope=all`、`days_before_expiry`
|
||||
- **AND** `package_ids` 以空数组提交
|
||||
- **AND** 保存成功后页面 MUST 以响应中的 `config_version`、`updater`、`updated_at` 刷新展示
|
||||
|
||||
#### Scenario: 保存指定主套餐范围配置
|
||||
|
||||
- **WHEN** 用户设置适用范围为「指定主套餐」并选择若干当前可售主套餐后提交
|
||||
- **THEN** `package_ids` MUST 非空且只包含当前可售主套餐
|
||||
- **AND** 前端 MUST 携带 `scope=specified` 与非空 `package_ids` 提交
|
||||
|
||||
#### Scenario: 参数校验
|
||||
|
||||
- **WHEN** `enabled` 缺失、`scope` 非法、`scope=specified` 时 `package_ids` 为空或包含不可售套餐,或 `days_before_expiry` 超出 1–90
|
||||
- **THEN** 前端 MUST 阻止提交并给出校验提示
|
||||
|
||||
#### Scenario: 仅影响后续扫描
|
||||
|
||||
- **WHEN** 用户修改并保存配置
|
||||
- **THEN** 历史自动续费记录 MUST NOT 被重算
|
||||
- **AND** 新配置 MUST 只对保存后的扫描生效
|
||||
|
||||
### Requirement: 自动续费配置访问权限
|
||||
|
||||
Only super admin and platform accounts SHALL be allowed to query or save the asset wallet auto-renewal configuration. 代理、企业与个人客户等身份访问时 SHALL 返回 `403` 并提示「无权限操作该资源或资源不存在」;无权限与不存在的表现 MUST NOT 可区分。
|
||||
|
||||
#### Scenario: 授权访问
|
||||
|
||||
- **WHEN** 超级管理员或平台账号访问配置接口
|
||||
- **THEN** 页面 MUST 正常查询与保存配置
|
||||
|
||||
#### Scenario: 无权限访问
|
||||
|
||||
- **WHEN** 代理、企业或个人客户账号访问配置接口
|
||||
- **THEN** 页面 MUST 按 `403` 无权限处理并展示统一提示
|
||||
- **AND** 前端 MUST NOT 通过响应区分无权限与资源不存在
|
||||
|
||||
### Requirement: 自动续费失败后台通知
|
||||
|
||||
The admin notification center SHALL display the new notification type `asset.auto_renewal.failed`(类别 `expiry`、级别 `warning`)through the existing notification APIs,接收人为业务员/店铺账号。目标 `target_type` 的 `iot_card` 与 `device` SHALL 位于通知导航白名单;当目标 `available=false` 时页面 SHALL 只展示正文且不跳转;该类型通知 SHALL 计入未读数。
|
||||
|
||||
#### Scenario: 展示并跳转自动续费失败通知
|
||||
|
||||
- **WHEN** 通知列表返回 `asset.auto_renewal.failed` 类型且目标可用
|
||||
- **THEN** 通知中心 MUST 将该通知归入 `expiry` 类别展示
|
||||
- **AND** 用户点击后 MUST 通过既有目标接口按 `iot_card` / `device` 跳转到对应资产详情
|
||||
|
||||
#### Scenario: 目标不可用
|
||||
|
||||
- **WHEN** 目标接口返回 `available=false`
|
||||
- **THEN** 页面 MUST 只展示通知正文且不发起跳转
|
||||
|
||||
### Requirement: H5 客户侧通知边界
|
||||
|
||||
This capability SHALL NOT 新增或修改 H5 客户侧接口与页面。`asset.auto_renewal.failed` 类型已由后端加入客户可见白名单,通过既有 `GET /api/c/v1/notifications` 与 `unread-count` 返回;客户侧仅展示文案(资产标识、套餐、原因、到期日),无跳转接口、无需新页面。该类型属 `expiry` 类别,展示期以业务到期时间为准,套餐到期后从列表消失(既有类别行为)。
|
||||
|
||||
#### Scenario: H5 复用既有接口
|
||||
|
||||
- **WHEN** 客户侧通知接口返回该类型
|
||||
- **THEN** 客户侧 MUST 仅展示文案且不提供跳转
|
||||
- **AND** 本次变更 MUST NOT 新增 H5 客户侧接口或页面
|
||||
|
||||
#### Scenario: 到期展示期
|
||||
|
||||
- **WHEN** 对应套餐业务到期
|
||||
- **THEN** 该通知按 `expiry` 类别既有行为从客户侧列表消失
|
||||
|
||||
### Requirement: 功能范围边界
|
||||
|
||||
This capability SHALL NOT 包含以下内容:自动续费记录页/异常记录页、尝试记录查询、「有余额就不停机」开关、H5 客户侧新接口。
|
||||
|
||||
#### Scenario: 明确不提供的功能
|
||||
|
||||
- **WHEN** 前端对接本次资产钱包自动续费能力
|
||||
- **THEN** 页面 MUST NOT 提供或依赖自动续费记录页、异常记录页、尝试记录查询接口与「有余额就不停机」开关
|
||||
@@ -0,0 +1,27 @@
|
||||
# Tasks: 资产钱包自动续费配置
|
||||
|
||||
## 1. 类型与 API
|
||||
- [ ] 1.1 新增 `src/types/api/assetWallet.ts`:`AssetAutoRenewalConfig`(`enabled`/`enabled_name`/`scope`/`scope_name`/`package_ids`/`days_before_expiry`/`config_version`/`updater`/`updated_at`)、`UpdateAssetAutoRenewalConfigRequest`(`enabled`/`scope`/`package_ids`/`days_before_expiry`)等类型
|
||||
- [ ] 1.2 `src/types/api/index.ts` 导出新类型
|
||||
- [ ] 1.3 新增 `src/api/modules/assetWallet.ts`:
|
||||
- `getAutoRenewalConfig`:`GET /api/admin/asset-auto-renewal-config`
|
||||
- `saveAutoRenewalConfig`:`PUT /api/admin/asset-auto-renewal-config`
|
||||
- [ ] 1.4 `src/api/modules/index.ts` 导出新服务
|
||||
|
||||
## 2. 配置页面
|
||||
- [ ] 2.1 `src/router/routesAlias.ts` 新增资产钱包自动续费配置路由别名,`src/router/routes/asyncRoutes.ts` 在设置下注册路由与菜单(仅超级管理员与平台账号可见)
|
||||
- [ ] 2.2 新增 `src/views/settings/asset-wallet-auto-renewal/index.vue`:读取配置并展示总开关、适用范围、指定主套餐、统一到期前天数、配置版本、最近保存操作者与时间
|
||||
- [ ] 2.3 实现保存表单:总开关(0/1)、适用范围(all/specified)、到期前天数(1–90 校验);指定范围时主套餐多选仅支持当前可售主套餐且非空
|
||||
- [ ] 2.4 保存成功后使用响应刷新 `config_version`、`updater`、`updated_at`,不做新增/删除记录操作
|
||||
|
||||
## 3. 通知适配
|
||||
- [ ] 3.1 确认 `src/utils/business/notificationNavigation.ts` 白名单包含 `iot_card` 与 `device`,目标可用时跳转资产详情
|
||||
- [ ] 3.2 确认 `src/components/core/layouts/art-notification/index.vue` 的 `expiry` 分类可展示 `asset.auto_renewal.failed`,`available=false` 时只显示正文不跳转
|
||||
- [ ] 3.3 确认该类型通知计入未读数与分类汇总
|
||||
|
||||
## 4. Verification
|
||||
- [ ] 4.1 验证 GET/PUT 字段传递与参数校验(enabled、scope、package_ids、days_before_expiry)
|
||||
- [ ] 4.2 验证指定范围时 package_ids 非空且仅可售主套餐
|
||||
- [ ] 4.3 验证通知类型展示、目标跳转与不可用目标处理
|
||||
- [ ] 4.4 验证非超级管理员/平台账号访问的 403 处理
|
||||
- [ ] 4.5 运行 lint、类型检查与构建
|
||||
53
openspec/changes/add-polling-priority-queue/proposal.md
Normal file
53
openspec/changes/add-polling-priority-queue/proposal.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Change: 新增轮询管理 - 优先队列 API 前端对接(列表 + 人工优先入队 + 单项详情)
|
||||
|
||||
## Why
|
||||
|
||||
根据 `docs/产品迭代8月份/优先队列.md`。后端测试环境(`https://cmp-api.boss160.cn`,`Authorization: Bearer <token>`)已提供「轮询管理 - 优先队列」能力:
|
||||
|
||||
- 查询优先轮询项列表:`GET /api/admin/polling-priority-items`
|
||||
- 人工优先入队:`POST /api/admin/polling-priority-items`
|
||||
- 查询优先轮询项详情:`GET /api/admin/polling-priority-items/{id}`
|
||||
|
||||
该能力服务于:
|
||||
1. 查询优先轮询项:分页查询卡轮询优先项,按创建时间倒序,支持卡 `card_id`、任务类型 `task_type`、状态 `status`、触发类型 `trigger_type` 筛选;数据范围按卡所属店铺快照下推:代理只能看到自身及下级店铺资产,平台卡不可见。
|
||||
2. 人工优先入队:为该卡的全部纳入轮询任务类型(实名/流量/套餐/卡状态)建立或合并优先轮询项,原因必填(最长 500 字符)。`不受` 既有人工触发的每日次数上限与 24 小时去重约束;`不修改` 调度优先级,也 `不绕过` 既有并发上限。
|
||||
3. 查询优先轮询项详情:查询单条卡轮询优先项,越权与不存在返回同一响应,不产生可枚举差异,`不提供` 优先级分级、有效期与人工重触发入口。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增能力 `polling-priority-queue`,提供:
|
||||
- 优先轮询项列表:
|
||||
- 分页:`GET /api/admin/polling-priority-items`,按创建时间倒序;
|
||||
- 筛选:卡 `card_id`、任务类型 `task_type`、状态 `status`(`pending/processing/completed/failed`)、触发类型 `trigger_type`(含 `manual_trigger` 人工入队);
|
||||
- 列表项字段:`id`/`card_id`/`iccid`/`task_type(_name)`/`status(_name)`/`trigger_type(_name)`/`trigger_types`/`trigger_count`/`attempt_count`/`result(_name)`/`failure_reason`/`shop_id_snapshot`/`source_order_id`/`source_package_usage_id`/`manual_operator_id(_name)`/`manual_reason`/`claimed_at`/`last_triggered_at`/`created_at`/`updated_at`;
|
||||
- 人工优先入队:`POST /api/admin/polling-priority-items`:
|
||||
- 请求 `{card_id, reason}`,`reason` 必填、最长 500 字符;
|
||||
- 语义:为该卡的全部纳入轮询任务类型(实名/流量/套餐/卡状态)建立或合并优先轮询项;
|
||||
- 约束:同一卡同一任务类型至多一条活动项,重复入队合并 `trigger_count` 与来源集合;
|
||||
- 边界:`不受` 既有人工触发的每日次数上限与 24 小时去重约束;
|
||||
- 边界:`不修改` 调度优先级,也 `不绕过` 既有并发上限;
|
||||
- 响应:`{card_id, created_count, merged_count, task_types, task_type_names, items[{item_id, task_type(_name), status(_name), trigger_count, created, last_triggered_at}]`
|
||||
- 优先轮询项详情:`GET /api/admin/polling-priority-items/{id}`:
|
||||
- 越权与不存在返回同一响应,不产生可枚举差异;
|
||||
- `不提供` 优先级分级、有效期与人工重触发入口。
|
||||
- 任务类型 `task_type`:`polling:realname` 实名检查 / `polling:carddata` 流量检查 / `polling:package` 套餐检查 / `polling:card_status` 卡状态检查。
|
||||
- 状态 `status`:`pending` 待执行 / `processing` 执行中 / `completed` 已完成 / `failed` 失败出队。
|
||||
- 触发类型 `trigger_type`:`purchase_activated` 主套餐购买后立即生效 / `renewal_activated` 续购套餐生效 / `queue_activated` 排队主套餐顺延生效 / `addon_activated` 加油包生效 / `no_valid_package` 资产无有效套餐 / `manual_trigger` 人工入队。
|
||||
- 执行结果 `result`:`success` 成功 / `failed` 失败 / 空值表示未出结果。
|
||||
- 错误响应:`400` 请求参数错误 / `401` 未认证或认证已过期 / `403` 无权访问 / `500` 服务器内部错误。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- `polling-priority-queue` — 新增能力
|
||||
- Affected code:
|
||||
- `src/api/modules/pollingPriorityQueue.ts`(新增)
|
||||
- `src/api/modules/index.ts`
|
||||
- `src/types/api/pollingPriorityQueue.ts`(新增)
|
||||
- `src/types/api/index.ts`
|
||||
- `src/config/constants/augustIteration.ts`
|
||||
- `src/views/polling-management/priority-queue/index.vue`(新增)
|
||||
- `src/router/routesAlias.ts`
|
||||
- `src/router/routes/asyncRoutes.ts`
|
||||
- `src/locales/langs/zh.json`
|
||||
- `src/locales/langs/en.json`
|
||||
@@ -0,0 +1,79 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 优先轮询项列表
|
||||
|
||||
The admin frontend SHALL provide a paginated list of card polling priority items through `GET /api/admin/polling-priority-items`. The query SHALL support `page`(默认 1,最小 1)、`page_size`(默认 20,最大 100)以及筛选参数 `card_id`、`task_type`、`status`、`trigger_type`,并按创建时间倒序返回。
|
||||
|
||||
#### Scenario: 分页查询优先轮询项
|
||||
|
||||
- **WHEN** 用户进入优先队列页面或提交筛选条件
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/polling-priority-items`
|
||||
- **AND** 携带 `page`、`page_size` 及所选筛选参数
|
||||
- **AND** 页面 MUST 使用响应中的 `data.items`、`data.page`、`data.size` 和 `data.total` 渲染列表
|
||||
|
||||
#### Scenario: 数据范围下推
|
||||
|
||||
- **WHEN** 当前登录账号为代理
|
||||
- **THEN** 列表 MUST 只返回自身及下级店铺资产对应的优先轮询项
|
||||
- **AND** 平台卡对应的优先轮询项 MUST NOT 出现在列表中
|
||||
|
||||
### Requirement: 人工优先入队
|
||||
|
||||
The admin frontend SHALL enqueue a card for priority polling through `POST /api/admin/polling-priority-items` with request body `{ card_id, reason }`. The `reason` SHALL be required and no longer than 500 characters. The operation SHALL create or merge priority items for all in-scope polling task types(实名/流量/套餐/卡状态)of the card. The frontend SHALL NOT apply any existing manual daily quota or 24-hour deduplication constraints, SHALL NOT modify scheduling priority, and SHALL NOT bypass existing concurrency limits.
|
||||
|
||||
#### Scenario: 人工入队成功
|
||||
|
||||
- **WHEN** 用户填写卡号与原因并提交人工入队
|
||||
- **THEN** 前端 MUST 调用 `POST /api/admin/polling-priority-items`
|
||||
- **AND** 请求体为 `{ "card_id": 卡ID, "reason": "加急原因" }`
|
||||
- **AND** 页面 MUST 展示响应中的 `created_count`、`merged_count`、`task_types`、`task_type_names` 与 `items`
|
||||
|
||||
#### Scenario: 重复入队合并
|
||||
|
||||
- **WHEN** 同一卡同一任务类型已存在活动优先轮询项且再次入队
|
||||
- **THEN** 该卡该任务类型至多保持一条活动项
|
||||
- **AND** 响应 MUST 返回合并后的项并将该次入队计入 `trigger_count`
|
||||
|
||||
#### Scenario: 原因校验
|
||||
|
||||
- **WHEN** `reason` 为空或超过 500 字符
|
||||
- **THEN** 前端 MUST 阻止提交并给出校验提示
|
||||
|
||||
### Requirement: 优先轮询项详情
|
||||
|
||||
The admin frontend SHALL query a single priority polling item through `GET /api/admin/polling-priority-items/{id}`. 越权与不存在的响应 MUST 保持一致,不产生可枚举差异。该能力 SHALL NOT 提供优先级分级、有效期与人工重触发入口。
|
||||
|
||||
#### Scenario: 查询单项详情
|
||||
|
||||
- **WHEN** 用户查看某条优先轮询项详情
|
||||
- **THEN** 前端 MUST 调用 `GET /api/admin/polling-priority-items/{id}`
|
||||
- **AND** 页面 MUST 展示卡、任务类型、状态、触发类型、执行结果、失败原因、操作者、触发与尝试次数及相关时间字段
|
||||
|
||||
#### Scenario: 越权与不存在不区分
|
||||
|
||||
- **WHEN** 当前账号无权访问该记录或该记录不存在
|
||||
- **THEN** 前端 MUST 按同一错误处理,不得通过响应差异区分越权与不存在
|
||||
|
||||
### Requirement: 枚举与状态映射
|
||||
|
||||
The admin frontend SHALL map and display the backend enums for task type, status, trigger type and result with their Chinese labels.
|
||||
|
||||
#### Scenario: 任务类型展示
|
||||
|
||||
- **WHEN** 列表返回 `task_type` 为 `polling:realname` / `polling:carddata` / `polling:package` / `polling:card_status`
|
||||
- **THEN** 页面 MUST 分别展示为 实名检查 / 流量检查 / 套餐检查 / 卡状态检查
|
||||
|
||||
#### Scenario: 状态与触发类型展示
|
||||
|
||||
- **WHEN** 列表返回 `status`(`pending`/`processing`/`completed`/`failed`)、`trigger_type`(`purchase_activated`/`renewal_activated`/`queue_activated`/`addon_activated`/`no_valid_package`/`manual_trigger`)与 `result`(`success`/`failed`/空)
|
||||
- **THEN** 页面 MUST 展示对应的中文名称与枚举值
|
||||
- **AND** `result` 为空时 MUST 展示为未出结果
|
||||
|
||||
### Requirement: 错误响应处理
|
||||
|
||||
The admin frontend SHALL handle the documented error responses for the priority queue APIs: `400` 请求参数错误、`401` 未认证或认证已过期、`403` 无权访问、`500` 服务器内部错误。
|
||||
|
||||
#### Scenario: 权限与参数错误提示
|
||||
|
||||
- **WHEN** 接口返回 `401`、`403` 或参数校验错误
|
||||
- **THEN** 页面 MUST 展示对应错误提示且不产生前端异常
|
||||
30
openspec/changes/add-polling-priority-queue/tasks.md
Normal file
30
openspec/changes/add-polling-priority-queue/tasks.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Tasks: 轮询管理 - 优先队列
|
||||
|
||||
## 1. 类型
|
||||
- [ ] 1.1 新增 `src/types/api/pollingPriorityQueue.ts`:优先轮询项 `PollingPriorityItem`(`id` / `card_id` / `iccid` / `task_type` / `task_type_name` / `status` / `status_name` / `trigger_type` / `trigger_type_name` / `trigger_types` / `trigger_count` / `attempt_count` / `result` / `result_name` / `failure_reason` / `shop_id_snapshot` / `source_order_id` / `source_package_usage_id` / `manual_operator_id` / `manual_operator_name` / `manual_reason` / `claimed_at` / `last_triggered_at` / `created_at` / `updated_at`)
|
||||
- [ ] 1.2 查询参数、分页响应 `PollingPriorityItemPageResult`、人工入队请求/响应、入队项 `PriorityItemResult` 类型
|
||||
- [ ] 1.3 `src/types/api/index.ts` 导出新类型
|
||||
|
||||
## 2. API
|
||||
- [ ] 2.1 新增 `src/api/modules/pollingPriorityQueue.ts`:
|
||||
- `getPriorityItems`:`GET /api/admin/polling-priority-items`,分页查询卡轮询优先项,按创建时间倒序,支持卡 `card_id`、任务类型 `task_type`、状态 `status`、触发类型 `trigger_type` 筛选
|
||||
- `createPriorityItems`:`POST /api/admin/polling-priority-items`,人工优先入队,请求 `{ card_id, reason }`
|
||||
- `getPriorityItemDetail`:`GET /api/admin/polling-priority-items/{id}`,查询单项详情
|
||||
- [ ] 2.2 `src/api/modules/index.ts` 导出新服务
|
||||
|
||||
## 3. 页面
|
||||
- [ ] 3.1 `src/router/routesAlias.ts` 新增优先队列路由别名,`src/router/routes/asyncRoutes.ts` 在轮询管理下注册路由与菜单
|
||||
- [ ] 3.2 新增 `src/views/polling-management/priority-queue/index.vue`:筛选(卡/任务类型/状态/触发类型)、分页、表格展示列表项字段
|
||||
- [ ] 3.3 实现任务类型、状态、触发类型、执行结果枚举映射与中文展示
|
||||
- [ ] 3.4 实现人工优先入队弹窗:选择卡、原因必填校验(最长 500 字符),提交后展示 created_count / merged_count / items
|
||||
- [ ] 3.5 实现单项详情查看(页面或抽屉),越权与不存在按同一错误处理
|
||||
|
||||
## 4. 常量与文案
|
||||
- [ ] 4.1 在 `src/config/constants/augustIteration.ts`(或新增常量文件)登记任务类型/状态/触发类型/结果枚举与中文名称
|
||||
- [ ] 4.2 `src/locales/langs/zh.json`、`src/locales/langs/en.json` 补充菜单与页面文案
|
||||
|
||||
## 5. Verification
|
||||
- [ ] 5.1 验证筛选与分页参数正确传递、列表按创建时间倒序
|
||||
- [ ] 5.2 验证人工入队成功/合并/原因校验(空与超长)
|
||||
- [ ] 5.3 验证详情接口与越权/不存在不区分
|
||||
- [ ] 5.4 运行 lint、类型检查与构建
|
||||
27
src/api/modules/assetWallet.ts
Normal file
27
src/api/modules/assetWallet.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
AssetAutoRenewalConfig,
|
||||
UpdateAssetAutoRenewalConfigRequest
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* 资产钱包相关服务
|
||||
*/
|
||||
export class AssetWalletService extends BaseService {
|
||||
/**
|
||||
* 查询自动续费配置
|
||||
*/
|
||||
static getAutoRenewalConfig(): Promise<BaseResponse<AssetAutoRenewalConfig>> {
|
||||
return this.get<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config')
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存自动续费配置
|
||||
*/
|
||||
static saveAutoRenewalConfig(
|
||||
data: UpdateAssetAutoRenewalConfigRequest
|
||||
): Promise<BaseResponse<AssetAutoRenewalConfig>> {
|
||||
return this.put<BaseResponse<AssetAutoRenewalConfig>>('/api/admin/asset-auto-renewal-config', data)
|
||||
}
|
||||
}
|
||||
@@ -53,3 +53,7 @@ export { H5PopupConfigurationService } from './h5PopupConfiguration'
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
export { PackageTrafficAlertService } from './packageTrafficAlert'
|
||||
|
||||
export { PollingPriorityQueueService } from './pollingPriorityQueue'
|
||||
|
||||
export { AssetWalletService } from './assetWallet'
|
||||
|
||||
53
src/api/modules/pollingPriorityQueue.ts
Normal file
53
src/api/modules/pollingPriorityQueue.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 轮询优先队列 API
|
||||
*/
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
BaseResponse,
|
||||
PollingPriorityItem,
|
||||
PollingPriorityItemPageResult,
|
||||
PollingPriorityItemQueryParams,
|
||||
CreatePollingPriorityItemRequest,
|
||||
CreatePollingPriorityItemResponse
|
||||
} from '@/types/api'
|
||||
|
||||
/**
|
||||
* 轮询优先队列服务
|
||||
*/
|
||||
export class PollingPriorityQueueService extends BaseService {
|
||||
/**
|
||||
* 获取优先轮询项列表
|
||||
* GET /api/admin/polling-priority-items
|
||||
* 按创建时间倒序返回,支持卡ID、任务类型、状态、触发类型筛选
|
||||
*/
|
||||
static getPriorityItems(
|
||||
params?: PollingPriorityItemQueryParams
|
||||
): Promise<BaseResponse<PollingPriorityItemPageResult>> {
|
||||
return this.get<BaseResponse<PollingPriorityItemPageResult>>(
|
||||
'/api/admin/polling-priority-items',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工优先入队
|
||||
* POST /api/admin/polling-priority-items
|
||||
* 请求体 { card_id, reason },为卡片全部范围内的轮询任务类型创建或合并优先项
|
||||
*/
|
||||
static createPriorityItems(
|
||||
data: CreatePollingPriorityItemRequest
|
||||
): Promise<BaseResponse<CreatePollingPriorityItemResponse>> {
|
||||
return this.post<BaseResponse<CreatePollingPriorityItemResponse>>(
|
||||
'/api/admin/polling-priority-items',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取优先轮询项详情
|
||||
* GET /api/admin/polling-priority-items/{id}
|
||||
*/
|
||||
static getPriorityItemDetail(id: number): Promise<BaseResponse<PollingPriorityItem>> {
|
||||
return this.get<BaseResponse<PollingPriorityItem>>(`/api/admin/polling-priority-items/${id}`)
|
||||
}
|
||||
}
|
||||
@@ -43,3 +43,6 @@ export * from './packageTrafficAlert'
|
||||
export * from './bulkPurchase'
|
||||
export * from './julyIteration'
|
||||
export * from './augustIteration'
|
||||
|
||||
// 轮询优先队列相关
|
||||
export * from './pollingPriorityQueue'
|
||||
|
||||
71
src/config/constants/pollingPriorityQueue.ts
Normal file
71
src/config/constants/pollingPriorityQueue.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 轮询优先队列相关常量
|
||||
* 对应 docs/产品迭代8月份/优先队列.md
|
||||
*/
|
||||
|
||||
import type {
|
||||
PollingPriorityTaskType,
|
||||
PollingPriorityItemStatus,
|
||||
PollingPriorityTriggerType,
|
||||
PollingPriorityResult
|
||||
} from '@/types/api'
|
||||
|
||||
/** 任务类型选项 */
|
||||
export const POLLING_PRIORITY_TASK_TYPE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PollingPriorityTaskType
|
||||
}> = [
|
||||
{ label: '实名检查', value: 'polling:realname' },
|
||||
{ label: '流量检查', value: 'polling:carddata' },
|
||||
{ label: '套餐检查', value: 'polling:package' },
|
||||
{ label: '卡状态检查', value: 'polling:card_status' }
|
||||
]
|
||||
|
||||
export const getPollingPriorityTaskTypeName = (
|
||||
taskType?: PollingPriorityTaskType | string | null
|
||||
) =>
|
||||
POLLING_PRIORITY_TASK_TYPE_OPTIONS.find((item) => item.value === taskType)?.label ||
|
||||
String(taskType || '-')
|
||||
|
||||
/** 状态选项 */
|
||||
export const POLLING_PRIORITY_STATUS_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PollingPriorityItemStatus
|
||||
}> = [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '失败', value: 'failed' }
|
||||
]
|
||||
|
||||
export const getPollingPriorityStatusName = (
|
||||
status?: PollingPriorityItemStatus | string | null
|
||||
) =>
|
||||
POLLING_PRIORITY_STATUS_OPTIONS.find((item) => item.value === status)?.label ||
|
||||
String(status || '-')
|
||||
|
||||
/** 触发类型选项 */
|
||||
export const POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PollingPriorityTriggerType
|
||||
}> = [
|
||||
{ label: '购买激活', value: 'purchase_activated' },
|
||||
{ label: '续费激活', value: 'renewal_activated' },
|
||||
{ label: '队列激活', value: 'queue_activated' },
|
||||
{ label: '附加套餐激活', value: 'addon_activated' },
|
||||
{ label: '无有效套餐', value: 'no_valid_package' },
|
||||
{ label: '人工触发', value: 'manual_trigger' }
|
||||
]
|
||||
|
||||
export const getPollingPriorityTriggerTypeName = (
|
||||
triggerType?: PollingPriorityTriggerType | string | null
|
||||
) =>
|
||||
POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS.find((item) => item.value === triggerType)?.label ||
|
||||
String(triggerType || '-')
|
||||
|
||||
/** 执行结果名称(result 为空时展示为未出结果) */
|
||||
export const getPollingPriorityResultName = (result?: PollingPriorityResult | string | null) => {
|
||||
if (result === 'success') return '成功'
|
||||
if (result === 'failed') return '失败'
|
||||
return '未出结果'
|
||||
}
|
||||
@@ -1216,6 +1216,16 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
title: '轮询监控',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 优先队列
|
||||
{
|
||||
path: 'priority-queue',
|
||||
name: 'PollingPriorityQueue',
|
||||
component: RoutesAlias.PollingPriorityQueue,
|
||||
meta: {
|
||||
title: '优先队列',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ export enum RoutesAlias {
|
||||
PollingConfig = '/polling-management/config', // 轮询配置
|
||||
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
|
||||
PollingMonitor = '/polling-management/monitor', // 轮询监控
|
||||
PollingPriorityQueue = '/polling-management/priority-queue', // 优先队列
|
||||
|
||||
// 公共组件
|
||||
Success = '/result/success', // 成功
|
||||
|
||||
46
src/types/api/assetWallet.ts
Normal file
46
src/types/api/assetWallet.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 资产钱包相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 自动续费适用范围
|
||||
*/
|
||||
export type AssetAutoRenewalScope = 'all' | 'specified'
|
||||
|
||||
/**
|
||||
* 资产钱包自动续费配置对象
|
||||
*/
|
||||
export interface AssetAutoRenewalConfig {
|
||||
/** 总开关(1:开启,0:关闭) */
|
||||
enabled: 0 | 1
|
||||
/** 总开关名称 */
|
||||
enabled_name: string
|
||||
/** 适用范围(all:全部主套餐,specified:指定主套餐) */
|
||||
scope: AssetAutoRenewalScope
|
||||
/** 适用范围名称 */
|
||||
scope_name: string
|
||||
/** 指定主套餐ID列表(scope=specified 时非空) */
|
||||
package_ids: number[]
|
||||
/** 统一到期前天数(1-90) */
|
||||
days_before_expiry: number
|
||||
/** 配置版本 */
|
||||
config_version: number
|
||||
/** 最近保存操作者 */
|
||||
updater: string
|
||||
/** 最近保存时间 */
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新资产钱包自动续费配置请求参数
|
||||
*/
|
||||
export interface UpdateAssetAutoRenewalConfigRequest {
|
||||
/** 总开关(0/1,必填) */
|
||||
enabled: 0 | 1
|
||||
/** 适用范围(all/specified,必填) */
|
||||
scope: AssetAutoRenewalScope
|
||||
/** 指定主套餐ID列表(scope=specified 时必填非空且仅当前可售主套餐) */
|
||||
package_ids: number[]
|
||||
/** 统一到期前天数(1-90,必填) */
|
||||
days_before_expiry: number
|
||||
}
|
||||
@@ -147,3 +147,9 @@ export * from './h5PopupConfiguration'
|
||||
|
||||
// 套餐真流量预警相关
|
||||
export * from './packageTrafficAlert'
|
||||
|
||||
// 轮询优先队列相关
|
||||
export * from './pollingPriorityQueue'
|
||||
|
||||
// 资产钱包自动续费相关
|
||||
export * from './assetWallet'
|
||||
|
||||
171
src/types/api/pollingPriorityQueue.ts
Normal file
171
src/types/api/pollingPriorityQueue.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 轮询优先队列相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 优先轮询任务类型
|
||||
*/
|
||||
export type PollingPriorityTaskType =
|
||||
| 'polling:realname'
|
||||
| 'polling:carddata'
|
||||
| 'polling:package'
|
||||
| 'polling:card_status'
|
||||
|
||||
/**
|
||||
* 优先轮询项状态
|
||||
*/
|
||||
export type PollingPriorityItemStatus = 'pending' | 'processing' | 'completed' | 'failed'
|
||||
|
||||
/**
|
||||
* 优先轮询项触发类型
|
||||
*/
|
||||
export type PollingPriorityTriggerType =
|
||||
| 'purchase_activated'
|
||||
| 'renewal_activated'
|
||||
| 'queue_activated'
|
||||
| 'addon_activated'
|
||||
| 'no_valid_package'
|
||||
| 'manual_trigger'
|
||||
|
||||
/**
|
||||
* 优先轮询执行结果
|
||||
*/
|
||||
export type PollingPriorityResult = 'success' | 'failed' | ''
|
||||
|
||||
/**
|
||||
* 优先轮询项对象
|
||||
*/
|
||||
export interface PollingPriorityItem {
|
||||
/** 优先轮询项ID */
|
||||
id: number
|
||||
/** 卡ID */
|
||||
card_id: number
|
||||
/** 卡ICCID */
|
||||
iccid: string
|
||||
/** 任务类型 */
|
||||
task_type: PollingPriorityTaskType
|
||||
/** 任务类型名称 */
|
||||
task_type_name: string
|
||||
/** 状态 */
|
||||
status: PollingPriorityItemStatus
|
||||
/** 状态名称 */
|
||||
status_name: string
|
||||
/** 触发类型 */
|
||||
trigger_type: PollingPriorityTriggerType
|
||||
/** 触发类型名称 */
|
||||
trigger_type_name: string
|
||||
/** 触发类型列表 */
|
||||
trigger_types: PollingPriorityTriggerType[]
|
||||
/** 触发次数 */
|
||||
trigger_count: number
|
||||
/** 尝试次数 */
|
||||
attempt_count: number
|
||||
/** 执行结果 */
|
||||
result: PollingPriorityResult
|
||||
/** 执行结果名称 */
|
||||
result_name: string
|
||||
/** 失败原因 */
|
||||
failure_reason: string | null
|
||||
/** 店铺快照ID */
|
||||
shop_id_snapshot: number | null
|
||||
/** 来源订单ID */
|
||||
source_order_id: number | null
|
||||
/** 来源套餐使用记录ID */
|
||||
source_package_usage_id: number | null
|
||||
/** 人工操作者ID */
|
||||
manual_operator_id: number | null
|
||||
/** 人工操作者名称 */
|
||||
manual_operator_name: string | null
|
||||
/** 人工入队原因 */
|
||||
manual_reason: string | null
|
||||
/** 认领时间 */
|
||||
claimed_at: string | null
|
||||
/** 最后触发时间 */
|
||||
last_triggered_at: string | null
|
||||
/** 创建时间 */
|
||||
created_at: string
|
||||
/** 更新时间 */
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先轮询项查询参数
|
||||
*/
|
||||
export interface PollingPriorityItemQueryParams {
|
||||
/** 页码(默认1,最小1) */
|
||||
page?: number
|
||||
/** 每页数量(默认20,最大100) */
|
||||
page_size?: number
|
||||
/** 卡ID筛选 */
|
||||
card_id?: number
|
||||
/** 任务类型筛选 */
|
||||
task_type?: PollingPriorityTaskType
|
||||
/** 状态筛选 */
|
||||
status?: PollingPriorityItemStatus
|
||||
/** 触发类型筛选 */
|
||||
trigger_type?: PollingPriorityTriggerType
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先轮询项分页响应
|
||||
*/
|
||||
export interface PollingPriorityItemPageResult {
|
||||
/** 优先轮询项列表 */
|
||||
items: PollingPriorityItem[]
|
||||
/** 当前页 */
|
||||
page: number
|
||||
/** 每页数量 */
|
||||
size: number
|
||||
/** 总数 */
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工优先入队请求参数
|
||||
*/
|
||||
export interface CreatePollingPriorityItemRequest {
|
||||
/** 卡ID */
|
||||
card_id: number
|
||||
/** 入队原因(必填,最长500字符) */
|
||||
reason: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工入队结果项
|
||||
*/
|
||||
export interface PriorityItemResult {
|
||||
/** 优先轮询项ID */
|
||||
id: number
|
||||
/** 卡ID */
|
||||
card_id: number
|
||||
/** 卡ICCID */
|
||||
iccid: string
|
||||
/** 任务类型 */
|
||||
task_type: PollingPriorityTaskType
|
||||
/** 任务类型名称 */
|
||||
task_type_name: string
|
||||
/** 状态 */
|
||||
status: PollingPriorityItemStatus
|
||||
/** 状态名称 */
|
||||
status_name: string
|
||||
/** 触发次数 */
|
||||
trigger_count: number
|
||||
/** 创建时间 */
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工优先入队响应
|
||||
*/
|
||||
export interface CreatePollingPriorityItemResponse {
|
||||
/** 新建数量 */
|
||||
created_count: number
|
||||
/** 合并数量 */
|
||||
merged_count: number
|
||||
/** 涉及任务类型列表 */
|
||||
task_types: PollingPriorityTaskType[]
|
||||
/** 涉及任务类型名称列表 */
|
||||
task_type_names: string[]
|
||||
/** 入队结果项列表 */
|
||||
items: PriorityItemResult[]
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="110"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
label-width="110"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
|
||||
@@ -300,7 +300,7 @@
|
||||
<ElFormItem label="主体类型" prop="subject_type">
|
||||
<ElRadioGroup v-model="qualificationForm.subject_type">
|
||||
<ElRadio label="enterprise">企业</ElRadio>
|
||||
<ElRadio label="individual">个人</ElRadio>
|
||||
<ElRadio label="personal">个人</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="主体证件号" prop="subject_code">
|
||||
@@ -367,11 +367,7 @@
|
||||
</ElFormItem>
|
||||
<div class="qualification-row">
|
||||
<ElFormItem label="发票抬头">
|
||||
<ElInput
|
||||
v-model="qualificationForm.invoice_title"
|
||||
placeholder="选填"
|
||||
maxlength="100"
|
||||
/>
|
||||
<ElInput v-model="qualificationForm.invoice_title" placeholder="选填" maxlength="100" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="发票主体证件号">
|
||||
<ElInput
|
||||
@@ -399,18 +395,28 @@
|
||||
<div v-loading="detailLoading" class="withdrawal-detail">
|
||||
<template v-if="withdrawalDetail">
|
||||
<ElDescriptions :column="1" border>
|
||||
<ElDescriptionsItem label="提现单号">{{ withdrawalDetail.withdrawal_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现单号">{{
|
||||
withdrawalDetail.withdrawal_no
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">{{ withdrawalDetail.status_name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现金额">{{ formatMoney(withdrawalDetail.amount) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实际到账">{{ formatMoney(withdrawalDetail.actual_amount) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手续费">{{ formatMoney(withdrawalDetail.fee) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现金额">{{
|
||||
formatMoney(withdrawalDetail.amount)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实际到账">{{
|
||||
formatMoney(withdrawalDetail.actual_amount)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="手续费">{{
|
||||
formatMoney(withdrawalDetail.fee)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现方式">
|
||||
{{ withdrawalMethodLabel(withdrawalDetail.withdrawal_method) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="收款账户">
|
||||
{{ withdrawalDetail.account_name }} / {{ withdrawalDetail.account_number }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="驳回原因">{{ withdrawalDetail.reject_reason || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="驳回原因">{{
|
||||
withdrawalDetail.reject_reason || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="异常标记">
|
||||
<ElTag :type="withdrawalDetail.anomaly_flag ? 'danger' : 'success'">
|
||||
{{ withdrawalDetail.anomaly_name }}
|
||||
@@ -442,12 +448,7 @@
|
||||
</ElDrawer>
|
||||
|
||||
<!-- 重新提交被驳回的提现 -->
|
||||
<ElDialog
|
||||
v-model="resubmitDialogVisible"
|
||||
title="重新提交提现"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElDialog v-model="resubmitDialogVisible" title="重新提交提现" width="560px" destroy-on-close>
|
||||
<ElForm
|
||||
ref="resubmitFormRef"
|
||||
:model="resubmitForm"
|
||||
@@ -503,11 +504,7 @@
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="resubmitSubmitting"
|
||||
@click="handleSubmitResubmit"
|
||||
>
|
||||
<ElButton type="primary" :loading="resubmitSubmitting" @click="handleSubmitResubmit">
|
||||
提交
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -529,7 +526,6 @@
|
||||
confirm-permission="commission_record:export"
|
||||
title="导出佣金记录"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -549,7 +545,7 @@
|
||||
WithdrawalQualificationSubmitParams,
|
||||
WithdrawalRequestDetail,
|
||||
ResubmitWithdrawalRequestParams,
|
||||
ShopCommissionRecordSource,
|
||||
ShopCommissionRecordSource
|
||||
} from '@/types/api/commission'
|
||||
import { WithdrawalMethod, WithdrawalStatus } from '@/types/api/commission'
|
||||
import type { Order, OrderQueryParams } from '@/types/api/order'
|
||||
@@ -1336,7 +1332,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// ==================== 提现资料资格 ====================
|
||||
|
||||
const qualificationLoading = ref(false)
|
||||
@@ -1371,54 +1366,52 @@
|
||||
return labels.length > 0 ? labels.join('、') : '-'
|
||||
}
|
||||
|
||||
const {
|
||||
columnChecks: qualificationColumnChecks,
|
||||
columns: qualificationColumns
|
||||
} = useCheckedColumns(() => [
|
||||
{ prop: 'subject_type_name', label: '主体类型', width: 90 },
|
||||
{ prop: 'subject_code_masked', label: '主体证件号', minWidth: 160 },
|
||||
{ prop: 'legal_person_id_card_masked', label: '法人身份证', minWidth: 170 },
|
||||
{
|
||||
prop: 'status_name',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: WithdrawalQualificationItem) => {
|
||||
const tagType = row.status === 0 ? 'warning' : row.status === 1 ? 'success' : 'danger'
|
||||
return h(ElTag, { type: tagType }, () => row.status_name || String(row.status))
|
||||
const { columnChecks: qualificationColumnChecks, columns: qualificationColumns } =
|
||||
useCheckedColumns(() => [
|
||||
{ prop: 'subject_type_name', label: '主体类型', width: 90 },
|
||||
{ prop: 'subject_code_masked', label: '主体证件号', minWidth: 160 },
|
||||
{ prop: 'legal_person_id_card_masked', label: '法人身份证', minWidth: 170 },
|
||||
{
|
||||
prop: 'status_name',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: WithdrawalQualificationItem) => {
|
||||
const tagType = row.status === 0 ? 'warning' : row.status === 1 ? 'success' : 'danger'
|
||||
return h(ElTag, { type: tagType }, () => row.status_name || String(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'approval_status_name',
|
||||
label: '审批状态',
|
||||
width: 100,
|
||||
formatter: (row: WithdrawalQualificationItem) => row.approval_status_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'attachments',
|
||||
label: '附件',
|
||||
minWidth: 200,
|
||||
formatter: (row: WithdrawalQualificationItem) => qualificationAttachmentLabels(row)
|
||||
},
|
||||
{
|
||||
prop: 'invalid_reason',
|
||||
label: '作废原因',
|
||||
minWidth: 160,
|
||||
formatter: (row: WithdrawalQualificationItem) => row.invalid_reason || '-'
|
||||
},
|
||||
{
|
||||
prop: 'invalidated_at',
|
||||
label: '作废时间',
|
||||
width: 170,
|
||||
formatter: (row: WithdrawalQualificationItem) =>
|
||||
row.invalidated_at ? formatDateTime(row.invalidated_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: WithdrawalQualificationItem) => formatDateTime(row.created_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'approval_status_name',
|
||||
label: '审批状态',
|
||||
width: 100,
|
||||
formatter: (row: WithdrawalQualificationItem) => row.approval_status_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'attachments',
|
||||
label: '附件',
|
||||
minWidth: 200,
|
||||
formatter: (row: WithdrawalQualificationItem) => qualificationAttachmentLabels(row)
|
||||
},
|
||||
{
|
||||
prop: 'invalid_reason',
|
||||
label: '作废原因',
|
||||
minWidth: 160,
|
||||
formatter: (row: WithdrawalQualificationItem) => row.invalid_reason || '-'
|
||||
},
|
||||
{
|
||||
prop: 'invalidated_at',
|
||||
label: '作废时间',
|
||||
width: 170,
|
||||
formatter: (row: WithdrawalQualificationItem) =>
|
||||
row.invalidated_at ? formatDateTime(row.invalidated_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 170,
|
||||
formatter: (row: WithdrawalQualificationItem) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
])
|
||||
|
||||
const getQualificationList = async () => {
|
||||
if (!currentShopId.value) return
|
||||
@@ -1562,8 +1555,7 @@
|
||||
confirmButtonText: '确认作废',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '作废原因(必填)',
|
||||
inputValidator: (reason: string) =>
|
||||
reason && reason.trim() ? true : '作废原因必填',
|
||||
inputValidator: (reason: string) => (reason && reason.trim() ? true : '作废原因必填'),
|
||||
type: 'warning'
|
||||
})
|
||||
await CommissionService.voidWithdrawalQualification(row.id, { reason: value.trim() })
|
||||
@@ -1571,8 +1563,8 @@
|
||||
getQualificationList()
|
||||
} catch {
|
||||
// 取消或请求失败
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 提现详情与重提 ====================
|
||||
|
||||
@@ -1591,10 +1583,7 @@
|
||||
detailLoading.value = true
|
||||
withdrawalDetail.value = null
|
||||
try {
|
||||
const res = await CommissionService.getWithdrawalRequestDetail(
|
||||
currentShopId.value,
|
||||
row.id
|
||||
)
|
||||
const res = await CommissionService.getWithdrawalRequestDetail(currentShopId.value, row.id)
|
||||
if (res.code === 0) {
|
||||
withdrawalDetail.value = res.data
|
||||
}
|
||||
@@ -1606,7 +1595,11 @@
|
||||
}
|
||||
|
||||
const getWithdrawalActions = (row: WithdrawalRequestItem) => {
|
||||
const actions: Array<{ label: string; handler: (row: any) => void; type?: 'primary' | 'danger' }> = [
|
||||
const actions: Array<{
|
||||
label: string
|
||||
handler: (row: any) => void
|
||||
type?: 'primary' | 'danger'
|
||||
}> = [
|
||||
{
|
||||
label: '详情',
|
||||
handler: () => openWithdrawalDetail(row),
|
||||
@@ -1755,7 +1748,6 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.qualification-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
]
|
||||
}
|
||||
if (isActing.value) {
|
||||
base.acting_reason = [{ required: true, message: '请填写代办原因', trigger: 'blur' }]
|
||||
base.acting_reason = [{ required: false, message: '请填写代办原因', trigger: 'blur' }]
|
||||
}
|
||||
return base
|
||||
})
|
||||
|
||||
492
src/views/polling-management/priority-queue/index.vue
Normal file
492
src/views/polling-management/priority-queue/index.vue
Normal file
@@ -0,0 +1,492 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="priority-queue-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<el-card shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<el-button type="primary" @click="showCreateDialog">人工优先入队</el-button>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="100"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 人工优先入队 -->
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="人工优先入队"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<el-form-item label="卡ICCID" prop="card_id">
|
||||
<el-select
|
||||
v-model="createForm.card_id"
|
||||
placeholder="请输入ICCID搜索"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="remoteSearchCard"
|
||||
clearable
|
||||
no-data-text="暂无卡数据,请输入ICCID搜索"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="card in cardList"
|
||||
:key="card.id"
|
||||
:label="card.iccid"
|
||||
:value="card.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="入队原因" prop="reason">
|
||||
<el-input
|
||||
v-model="createForm.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入入队原因(最多500字)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="handleCreateSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 入队结果 -->
|
||||
<el-dialog v-model="resultDialogVisible" title="入队结果" width="620px">
|
||||
<div class="result-summary">
|
||||
<el-tag type="success" class="result-item">新建 {{ createResult?.created_count ?? 0 }} 条</el-tag>
|
||||
<el-tag type="warning" class="result-item">合并 {{ createResult?.merged_count ?? 0 }} 条</el-tag>
|
||||
<span class="result-item">
|
||||
任务类型:{{ (createResult?.task_type_names || []).join('、') || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="createResult?.items || []" border stripe size="small" max-height="360">
|
||||
<el-table-column prop="iccid" label="卡ICCID" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="task_type_name" label="任务类型" min-width="120" />
|
||||
<el-table-column prop="status_name" label="状态" width="100" />
|
||||
<el-table-column prop="trigger_count" label="触发次数" width="100" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 优先轮询项详情 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="优先轮询项详情"
|
||||
width="640px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-descriptions v-if="detailData" :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ detailData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卡ICCID">{{ detailData.iccid }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务类型">
|
||||
{{ detailData.task_type_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusTagType(detailData.status)">{{
|
||||
detailData.status_name
|
||||
}}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="触发类型">
|
||||
{{ detailData.trigger_type_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="触发次数">{{ detailData.trigger_count }}</el-descriptions-item>
|
||||
<el-descriptions-item label="尝试次数">{{ detailData.attempt_count }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行结果">
|
||||
{{ detailData.result_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="失败原因" :span="2">
|
||||
{{ detailData.failure_reason || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="人工操作者">
|
||||
{{ detailData.manual_operator_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="入队原因">
|
||||
{{ detailData.manual_reason || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="认领时间">
|
||||
{{ formatDateTime(detailData.claimed_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后触发时间">
|
||||
{{ formatDateTime(detailData.last_triggered_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(detailData.created_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ formatDateTime(detailData.updated_at) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||
import { ElMessage, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { PollingPriorityQueueService, CardService } from '@/api/modules'
|
||||
import type {
|
||||
PollingPriorityItem,
|
||||
PollingPriorityItemQueryParams,
|
||||
CreatePollingPriorityItemResponse
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import {
|
||||
POLLING_PRIORITY_TASK_TYPE_OPTIONS,
|
||||
POLLING_PRIORITY_STATUS_OPTIONS,
|
||||
POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS,
|
||||
getPollingPriorityResultName
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'PollingPriorityQueue' })
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingPriorityItem[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
card_id: undefined as number | undefined,
|
||||
task_type: undefined as string | undefined,
|
||||
status: undefined as string | undefined,
|
||||
trigger_type: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '卡ID',
|
||||
prop: 'card_id',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '请输入卡ID' }
|
||||
},
|
||||
{
|
||||
label: '任务类型',
|
||||
prop: 'task_type',
|
||||
type: 'select',
|
||||
options: POLLING_PRIORITY_TASK_TYPE_OPTIONS,
|
||||
config: { clearable: true, placeholder: '请选择任务类型' }
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
options: POLLING_PRIORITY_STATUS_OPTIONS,
|
||||
config: { clearable: true, placeholder: '请选择状态' }
|
||||
},
|
||||
{
|
||||
label: '触发类型',
|
||||
prop: 'trigger_type',
|
||||
type: 'select',
|
||||
options: POLLING_PRIORITY_TRIGGER_TYPE_OPTIONS,
|
||||
config: { clearable: true, placeholder: '请选择触发类型' }
|
||||
}
|
||||
]
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
const types: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
|
||||
pending: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
failed: 'danger'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const getResultTagType = (result: string) => {
|
||||
if (result === 'success') return 'success'
|
||||
if (result === 'failed') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: '卡ICCID',
|
||||
minWidth: 170,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'task_type_name',
|
||||
label: '任务类型',
|
||||
width: 130,
|
||||
formatter: (row: PollingPriorityItem) => row.task_type_name
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: getStatusTagType(row.status) }, () => row.status_name)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'trigger_type_name',
|
||||
label: '触发类型',
|
||||
width: 130,
|
||||
formatter: (row: PollingPriorityItem) => row.trigger_type_name
|
||||
},
|
||||
{
|
||||
prop: 'trigger_count',
|
||||
label: '触发次数',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'attempt_count',
|
||||
label: '尝试次数',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'result',
|
||||
label: '执行结果',
|
||||
width: 110,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: getResultTagType(row.result) }, () =>
|
||||
getPollingPriorityResultName(row.result)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'manual_operator_name',
|
||||
label: '操作者',
|
||||
width: 120,
|
||||
formatter: (row: PollingPriorityItem) => row.manual_operator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: PollingPriorityItem) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
const getActions = () => [{ label: '详情', handler: showDetail, type: 'primary' as const }]
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const queryParams: PollingPriorityItemQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
card_id: searchForm.card_id,
|
||||
task_type: searchForm.task_type as never,
|
||||
status: searchForm.status as never,
|
||||
trigger_type: searchForm.trigger_type as never
|
||||
}
|
||||
const { data } = await PollingPriorityQueueService.getPriorityItems(queryParams)
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载优先轮询项列表失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
card_id: undefined,
|
||||
task_type: undefined,
|
||||
status: undefined,
|
||||
trigger_type: undefined
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 人工优先入队
|
||||
const createDialogVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const createResult = ref<CreatePollingPriorityItemResponse | null>(null)
|
||||
const resultDialogVisible = ref(false)
|
||||
const cardList = ref<Array<{ id: number; iccid: string }>>([])
|
||||
|
||||
const createForm = reactive({
|
||||
card_id: undefined as number | undefined,
|
||||
reason: ''
|
||||
})
|
||||
|
||||
const createRules: FormRules = {
|
||||
card_id: [{ required: true, message: '请选择卡', trigger: 'change' }],
|
||||
reason: [
|
||||
{ required: true, message: '请输入入队原因', trigger: 'blur' },
|
||||
{ max: 500, message: '入队原因不能超过500个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
createForm.card_id = undefined
|
||||
createForm.reason = ''
|
||||
createResult.value = null
|
||||
createDialogVisible.value = true
|
||||
cardList.value = []
|
||||
loadCards()
|
||||
}
|
||||
|
||||
const loadCards = async (iccid?: string) => {
|
||||
try {
|
||||
const { data } = await CardService.getStandaloneIotCards({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
iccid
|
||||
})
|
||||
cardList.value = data.items || []
|
||||
} catch (error) {
|
||||
console.error('加载卡列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const remoteSearchCard = (query: string) => {
|
||||
loadCards(query)
|
||||
}
|
||||
|
||||
const handleCreateSubmit = async () => {
|
||||
if (!createFormRef.value) return
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (createForm.card_id === undefined) {
|
||||
ElMessage.warning('请选择卡')
|
||||
return
|
||||
}
|
||||
createLoading.value = true
|
||||
try {
|
||||
const { data } = await PollingPriorityQueueService.createPriorityItems({
|
||||
card_id: createForm.card_id,
|
||||
reason: createForm.reason.trim()
|
||||
})
|
||||
createResult.value = data
|
||||
createDialogVisible.value = false
|
||||
resultDialogVisible.value = true
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('人工优先入队失败', error)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<PollingPriorityItem | null>(null)
|
||||
|
||||
const showDetail = async (row: PollingPriorityItem) => {
|
||||
detailData.value = null
|
||||
detailDialogVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const { data } = await PollingPriorityQueueService.getPriorityItemDetail(row.id)
|
||||
detailData.value = data
|
||||
} catch (error) {
|
||||
console.error('加载优先轮询项详情失败', error)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.priority-queue-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.result-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.result-item {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user