fetch(add): 订单管理-企业设备
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m30s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m30s
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Change: 企业设备授权管理
|
||||
|
||||
## Why
|
||||
|
||||
当前系统中已存在企业客户管理和企业卡授权功能,但缺少企业设备授权管理能力。企业客户需要能够查看和管理被授权的设备列表,运营人员需要能够将设备授权给企业客户使用,并支持撤销授权操作。
|
||||
|
||||
根据业务需求文档 (`docs/企业设备授权.md`),需要在资产管理模块下新增"企业设备列表"功能,实现以下核心能力:
|
||||
|
||||
1. **授权设备给企业** - 支持批量授权,最多100个设备号
|
||||
2. **查看企业设备列表** - 支持分页和按设备号搜索
|
||||
3. **撤销设备授权** - 支持批量撤销授权
|
||||
|
||||
## What Changes
|
||||
|
||||
新增企业设备授权管理功能,包括:
|
||||
|
||||
### 类型定义
|
||||
- 新增 `src/types/api/enterpriseDevice.ts` 文件
|
||||
- 定义设备列表项、查询参数、分页结果类型
|
||||
- 定义授权/撤销请求和响应类型
|
||||
- 在 `src/types/api/index.ts` 中导出新类型
|
||||
|
||||
### API 服务层
|
||||
- 扩展 `EnterpriseService` 类,新增3个方法:
|
||||
- `allocateDevices(enterpriseId, data)` - POST 授权设备
|
||||
- `getEnterpriseDevices(enterpriseId, params)` - GET 设备列表
|
||||
- `recallDevices(enterpriseId, data)` - POST 撤销授权
|
||||
|
||||
### 视图层
|
||||
- 新增 `src/views/asset-management/enterprise-devices/index.vue` 页面
|
||||
- 实现设备列表展示 (表格、分页、搜索)
|
||||
- 实现授权设备对话框 (支持批量输入设备号)
|
||||
- 实现撤销授权功能 (二次确认)
|
||||
- 实现操作结果展示 (成功/失败统计)
|
||||
|
||||
### 路由配置
|
||||
- 在 `src/router/routesAlias.ts` 添加路由别名
|
||||
- 在 `src/router/routes/asyncRoutes.ts` 的资产管理模块下添加子路由
|
||||
|
||||
### 国际化
|
||||
- 在 `src/locales/langs/zh.json` 和 `en.json` 添加中英文翻译
|
||||
- 包含菜单、表单、表格、对话框、提示消息等所有文案
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增模块**: 企业设备授权管理
|
||||
- **影响范围**:
|
||||
- 新增文件: `enterpriseDevice.ts`, `enterprise-devices/index.vue`
|
||||
- 扩展文件: `enterprise.ts` (API), `routesAlias.ts`, `asyncRoutes.ts`, `zh.json`, `en.json`, `index.ts` (types)
|
||||
- **依赖关系**:
|
||||
- 依赖现有的 `EnterpriseService` 基础设施
|
||||
- 依赖已实现的设备管理模块
|
||||
- 后端 API 已就绪
|
||||
- **向后兼容性**: 完全兼容,不影响现有功能
|
||||
- **数据迁移**: 无需数据迁移
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
无破坏性变更。这是一个纯新增功能,不修改现有代码逻辑。
|
||||
|
||||
## Risks
|
||||
|
||||
- **低风险**: 功能独立,不影响现有企业卡授权功能
|
||||
- **低风险**: API 已定义清晰,类型安全有保障
|
||||
- **中风险**: 需要确保批量操作时的用户体验良好 (处理大量设备号输入和结果展示)
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **复用企业卡授权页面** - 不可行,设备和卡的数据结构和操作逻辑不同
|
||||
2. **在设备管理页面添加企业授权功能** - 不符合业务流程,企业授权属于资产管理范畴
|
||||
3. **使用单个设备号输入而非批量** - 不满足业务需求,运营人员需要批量授权能力
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. ✅ 设备号输入格式 - 支持换行或逗号分隔
|
||||
2. ✅ 最大批量数量 - API 限制最多100个设备号
|
||||
3. ✅ 失败情况处理 - API 返回成功/失败统计及失败原因列表
|
||||
4. ✅ 设备列表排序 - 按授权时间倒序
|
||||
|
||||
## References
|
||||
|
||||
- API 文档: `docs/企业设备授权.md`
|
||||
- 相关 OpenSpec 变更: `add-device-management`
|
||||
- 参考实现: 企业卡授权功能 (`enterprise-cards/index.vue`)
|
||||
@@ -0,0 +1,291 @@
|
||||
# Spec: Enterprise Device Authorization
|
||||
|
||||
## Overview
|
||||
|
||||
企业设备授权功能允许运营人员将设备授权给企业客户使用,并支持查看授权设备列表和撤销授权操作。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: System SHALL define enterprise device types
|
||||
|
||||
系统必须提供完整的企业设备授权相关类型定义,确保类型安全。
|
||||
|
||||
#### Scenario: 定义企业设备列表项类型
|
||||
|
||||
**Given** 需要展示企业设备列表
|
||||
**When** 定义 `EnterpriseDeviceItem` 接口
|
||||
**Then** 接口必须包含以下字段:
|
||||
- `device_id: number` - 设备ID
|
||||
- `device_no: string` - 设备号
|
||||
- `device_name: string` - 设备名称
|
||||
- `device_model: string` - 设备型号
|
||||
- `card_count: number` - 绑定卡数量
|
||||
- `authorized_at: string` - 授权时间
|
||||
|
||||
#### Scenario: 定义设备列表查询参数
|
||||
|
||||
**Given** 需要查询和搜索企业设备
|
||||
**When** 定义 `EnterpriseDeviceListParams` 接口
|
||||
**Then** 接口必须包含以下可选字段:
|
||||
- `page?: number` - 页码
|
||||
- `page_size?: number` - 每页数量
|
||||
- `device_no?: string` - 设备号模糊搜索
|
||||
|
||||
#### Scenario: 定义授权设备请求类型
|
||||
|
||||
**Given** 需要授权设备给企业
|
||||
**When** 定义 `AllocateDevicesRequest` 接口
|
||||
**Then** 接口必须包含:
|
||||
- `device_nos: string[]` - 设备号列表 (nullable, 最多100个)
|
||||
- `remark?: string` - 授权备注
|
||||
|
||||
#### Scenario: 定义授权设备响应类型
|
||||
|
||||
**Given** 授权操作需要返回详细结果
|
||||
**When** 定义 `AllocateDevicesResponse` 接口
|
||||
**Then** 接口必须包含:
|
||||
- `success_count: number` - 成功数量
|
||||
- `fail_count: number` - 失败数量
|
||||
- `authorized_devices: AuthorizedDeviceItem[]` - 已授权设备列表 (nullable)
|
||||
- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable)
|
||||
|
||||
**And** `AuthorizedDeviceItem` 包含:
|
||||
- `device_id: number` - 设备ID
|
||||
- `device_no: string` - 设备号
|
||||
- `card_count: number` - 绑定卡数量
|
||||
|
||||
**And** `FailedDeviceItem` 包含:
|
||||
- `device_no: string` - 设备号
|
||||
- `reason: string` - 失败原因
|
||||
|
||||
#### Scenario: 定义撤销授权请求类型
|
||||
|
||||
**Given** 需要撤销设备授权
|
||||
**When** 定义 `RecallDevicesRequest` 接口
|
||||
**Then** 接口必须包含:
|
||||
- `device_nos: string[]` - 设备号列表 (nullable, 最多100个)
|
||||
|
||||
#### Scenario: 定义撤销授权响应类型
|
||||
|
||||
**Given** 撤销操作需要返回结果统计
|
||||
**When** 定义 `RecallDevicesResponse` 接口
|
||||
**Then** 接口必须包含:
|
||||
- `success_count: number` - 成功数量
|
||||
- `fail_count: number` - 失败数量
|
||||
- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: System SHALL provide enterprise device authorization API services
|
||||
|
||||
系统必须提供企业设备授权相关的 API 服务方法。
|
||||
|
||||
#### Scenario: 授权设备给企业
|
||||
|
||||
**Given** 运营人员需要授权设备给企业客户
|
||||
**When** 调用 `EnterpriseService.allocateDevices(enterpriseId, data)`
|
||||
**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/allocate-devices`
|
||||
**And** 请求体必须包含设备号列表和可选备注
|
||||
**And** 返回授权结果,包含成功/失败统计和详细列表
|
||||
|
||||
#### Scenario: 获取企业设备列表
|
||||
|
||||
**Given** 需要查看企业的设备列表
|
||||
**When** 调用 `EnterpriseService.getEnterpriseDevices(enterpriseId, params)`
|
||||
**Then** 必须发送 GET 请求到 `/api/admin/enterprises/{id}/devices`
|
||||
**And** 支持分页参数 (page, page_size)
|
||||
**And** 支持设备号模糊搜索
|
||||
**And** 返回设备列表和总数
|
||||
|
||||
#### Scenario: 撤销设备授权
|
||||
|
||||
**Given** 需要撤销企业的设备授权
|
||||
**When** 调用 `EnterpriseService.recallDevices(enterpriseId, data)`
|
||||
**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/recall-devices`
|
||||
**And** 请求体必须包含设备号列表
|
||||
**And** 返回撤销结果,包含成功/失败统计和失败原因
|
||||
|
||||
---
|
||||
|
||||
### Requirement: System SHALL provide enterprise device list page
|
||||
|
||||
系统必须提供企业设备列表管理页面,支持查询、授权和撤销操作。
|
||||
|
||||
#### Scenario: 显示企业设备列表
|
||||
|
||||
**Given** 用户访问企业设备列表页面
|
||||
**When** 页面加载完成
|
||||
**Then** 必须显示设备列表表格
|
||||
**And** 表格必须包含以下列:
|
||||
- 设备ID
|
||||
- 设备号
|
||||
- 设备名称
|
||||
- 设备型号
|
||||
- 绑定卡数量
|
||||
- 授权时间
|
||||
|
||||
**And** 必须支持分页功能
|
||||
**And** 必须显示加载状态
|
||||
|
||||
#### Scenario: 搜索企业设备
|
||||
|
||||
**Given** 设备列表已加载
|
||||
**When** 用户在搜索框输入设备号
|
||||
**And** 点击搜索按钮
|
||||
**Then** 必须根据设备号模糊查询设备
|
||||
**And** 必须更新设备列表显示
|
||||
**And** 必须重置到第一页
|
||||
|
||||
#### Scenario: 授权设备对话框
|
||||
|
||||
**Given** 用户点击"授权设备"按钮
|
||||
**When** 授权设备对话框打开
|
||||
**Then** 必须显示设备号输入框 (textarea)
|
||||
**And** 必须显示备注输入框 (可选)
|
||||
**And** 必须提示支持的输入格式 (换行或逗号分隔)
|
||||
**And** 必须提示最多100个设备号限制
|
||||
**And** 必须有表单验证 (设备号必填)
|
||||
|
||||
#### Scenario: 提交授权设备
|
||||
|
||||
**Given** 用户在对话框中输入了设备号列表
|
||||
**When** 用户点击提交按钮
|
||||
**Then** 必须解析设备号列表 (支持换行和逗号分隔)
|
||||
**And** 必须去除空白字符和空行
|
||||
**And** 必须验证设备号数量不超过100个
|
||||
**And** 必须调用授权 API
|
||||
**And** 必须显示加载状态
|
||||
**And** 授权完成后必须展示结果:
|
||||
- 成功数量
|
||||
- 失败数量
|
||||
- 失败设备列表及原因
|
||||
|
||||
**And** 如果有成功授权的设备,必须刷新设备列表
|
||||
**And** 必须关闭对话框
|
||||
|
||||
#### Scenario: 撤销设备授权
|
||||
|
||||
**Given** 用户选中了要撤销的设备
|
||||
**When** 用户点击"撤销授权"按钮
|
||||
**Then** 必须显示二次确认对话框
|
||||
**And** 确认对话框必须显示将要撤销的设备数量
|
||||
|
||||
**When** 用户确认撤销
|
||||
**Then** 必须调用撤销 API
|
||||
**And** 必须显示加载状态
|
||||
**And** 撤销完成后必须展示结果:
|
||||
- 成功数量
|
||||
- 失败数量
|
||||
- 失败设备列表及原因
|
||||
|
||||
**And** 如果有成功撤销的设备,必须刷新设备列表
|
||||
|
||||
#### Scenario: 错误处理
|
||||
|
||||
**Given** API 调用可能失败
|
||||
**When** API 返回错误
|
||||
**Then** 必须显示友好的错误提示消息
|
||||
**And** 必须在控制台记录错误详情
|
||||
**And** 必须停止加载状态
|
||||
|
||||
#### Scenario: 分页切换
|
||||
|
||||
**Given** 设备列表超过一页
|
||||
**When** 用户切换页码或每页数量
|
||||
**Then** 必须保持当前的搜索条件
|
||||
**And** 必须重新加载设备列表
|
||||
**And** 必须显示加载状态
|
||||
|
||||
---
|
||||
|
||||
### Requirement: System SHALL configure routing for enterprise device list
|
||||
|
||||
系统必须为企业设备列表配置正确的路由。
|
||||
|
||||
#### Scenario: 注册企业设备列表路由
|
||||
|
||||
**Given** 需要访问企业设备列表页面
|
||||
**When** 配置路由
|
||||
**Then** 必须在资产管理模块 (`/asset-management`) 下添加子路由
|
||||
**And** 路由路径必须为 `enterprise-devices`
|
||||
**And** 路由名称必须为 `EnterpriseDevices`
|
||||
**And** 必须使用路由别名 `RoutesAlias.EnterpriseDevices`
|
||||
**And** 必须配置 meta 信息:
|
||||
- `title: 'menus.assetManagement.enterpriseDevices'`
|
||||
- `keepAlive: true`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: System SHALL provide internationalization support
|
||||
|
||||
系统必须提供中英文翻译支持。
|
||||
|
||||
#### Scenario: 中文翻译
|
||||
|
||||
**Given** 系统语言设置为中文
|
||||
**When** 访问企业设备相关页面
|
||||
**Then** 所有文本必须显示中文,包括:
|
||||
- 菜单标题: "企业设备列表"
|
||||
- 搜索表单标签和占位符
|
||||
- 表格列名
|
||||
- 按钮文本
|
||||
- 对话框标题和内容
|
||||
- 提示消息
|
||||
|
||||
#### Scenario: 英文翻译
|
||||
|
||||
**Given** 系统语言设置为英文
|
||||
**When** 访问企业设备相关页面
|
||||
**Then** 所有文本必须显示英文,包括:
|
||||
- 菜单标题: "Enterprise Devices"
|
||||
- 搜索表单标签和占位符
|
||||
- 表格列名
|
||||
- 按钮文本
|
||||
- 对话框标题和内容
|
||||
- 提示消息
|
||||
|
||||
---
|
||||
|
||||
### Requirement: System SHALL optimize user experience
|
||||
|
||||
系统必须提供良好的用户体验。
|
||||
|
||||
#### Scenario: 批量输入设备号
|
||||
|
||||
**Given** 用户需要授权多个设备
|
||||
**When** 在设备号输入框中输入
|
||||
**Then** 必须支持以下输入方式:
|
||||
- 每行一个设备号
|
||||
- 逗号分隔的设备号
|
||||
- 混合使用换行和逗号
|
||||
|
||||
**And** 系统必须能正确解析所有格式
|
||||
**And** 必须自动去除首尾空白字符
|
||||
**And** 必须过滤空行
|
||||
|
||||
#### Scenario: 操作结果展示
|
||||
|
||||
**Given** 批量操作完成
|
||||
**When** 显示操作结果
|
||||
**Then** 必须清晰展示:
|
||||
- 总共处理的数量
|
||||
- 成功的数量
|
||||
- 失败的数量
|
||||
- 每个失败项的设备号和失败原因
|
||||
|
||||
**And** 如果全部成功,必须显示成功提示
|
||||
**And** 如果部分失败,必须显示警告提示
|
||||
**And** 如果全部失败,必须显示错误提示
|
||||
|
||||
#### Scenario: 表格列管理
|
||||
|
||||
**Given** 设备列表表格已显示
|
||||
**When** 用户点击列管理按钮
|
||||
**Then** 必须能够选择显示/隐藏的列
|
||||
**And** 列配置必须被保存
|
||||
|
||||
## Related Specs
|
||||
|
||||
- 参考现有的企业卡授权功能 (enterpriseCard.ts)
|
||||
- 依赖现有的企业客户管理功能 (enterprise.ts)
|
||||
- 关联设备管理模块 (add-device-management)
|
||||
158
openspec/changes/add-enterprise-device-authorization/tasks.md
Normal file
158
openspec/changes/add-enterprise-device-authorization/tasks.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Tasks: Add Enterprise Device Authorization
|
||||
|
||||
## Overview
|
||||
|
||||
按照从底层到上层的顺序实现企业设备授权功能,确保每一步都可测试和验证。
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Type Definitions (Foundation)
|
||||
|
||||
1. **创建企业设备类型定义文件**
|
||||
- 创建 `src/types/api/enterpriseDevice.ts`
|
||||
- 定义 `EnterpriseDeviceItem` 接口 (设备列表项)
|
||||
- 定义 `EnterpriseDeviceListParams` 接口 (查询参数)
|
||||
- 定义 `EnterpriseDevicePageResult` 接口 (分页结果)
|
||||
- 定义 `AllocateDevicesRequest` 接口 (授权请求)
|
||||
- 定义 `AllocateDevicesResponse` 接口 (授权响应)
|
||||
- 定义 `AuthorizedDeviceItem` 接口 (已授权设备项)
|
||||
- 定义 `FailedDeviceItem` 接口 (失败设备项)
|
||||
- 定义 `RecallDevicesRequest` 接口 (撤销请求)
|
||||
- 定义 `RecallDevicesResponse` 接口 (撤销响应)
|
||||
- **验证**: 运行 `npm run type-check` 确保无类型错误
|
||||
|
||||
2. **导出类型定义**
|
||||
- 在 `src/types/api/index.ts` 中导出新增的类型
|
||||
- **验证**: 确认其他模块可以正确导入类型
|
||||
|
||||
### Phase 2: API Service Layer
|
||||
|
||||
3. **扩展 EnterpriseService API 方法**
|
||||
- 在 `src/api/modules/enterprise.ts` 中添加:
|
||||
- `allocateDevices(enterpriseId, data)` - 授权设备
|
||||
- `getEnterpriseDevices(enterpriseId, params)` - 获取设备列表
|
||||
- `recallDevices(enterpriseId, data)` - 撤销授权
|
||||
- 导入新增的类型定义
|
||||
- **验证**: 运行 `npm run type-check` 确保类型正确
|
||||
|
||||
### Phase 3: Internationalization
|
||||
|
||||
4. **添加中文翻译**
|
||||
- 在 `src/locales/langs/zh.json` 的 `menus.assetManagement` 下添加 `enterpriseDevices` 条目
|
||||
- 在 `src/locales/langs/zh.json` 添加 `enterpriseDevices` 模块的所有中文文案:
|
||||
- 页面标题和搜索表单
|
||||
- 表格列名
|
||||
- 操作按钮和对话框标题
|
||||
- 表单标签和提示文本
|
||||
- 成功/错误消息
|
||||
- **验证**: 检查 JSON 格式正确性
|
||||
|
||||
5. **添加英文翻译**
|
||||
- 在 `src/locales/langs/en.json` 添加对应的英文翻译
|
||||
- 确保 key 与中文版本一致
|
||||
- **验证**: 检查 JSON 格式正确性,切换语言测试
|
||||
|
||||
### Phase 4: Routing
|
||||
|
||||
6. **添加路由别名**
|
||||
- 在 `src/router/routesAlias.ts` 添加 `EnterpriseDevices = '/asset-management/enterprise-devices'`
|
||||
- **验证**: 确认导出正确
|
||||
|
||||
7. **配置路由**
|
||||
- 在 `src/router/routes/asyncRoutes.ts` 的资产管理 (`/asset-management`) 模块下添加企业设备列表路由
|
||||
- 配置路由 meta 信息 (title, keepAlive)
|
||||
- **验证**: 运行应用,检查路由注册成功
|
||||
|
||||
### Phase 5: UI Components
|
||||
|
||||
8. **创建企业设备列表页面**
|
||||
- 创建 `src/views/asset-management/enterprise-devices/index.vue`
|
||||
- 实现页面基础结构:
|
||||
- 使用 `ArtTableFullScreen` 布局
|
||||
- 使用 `ArtSearchBar` 实现搜索表单 (企业ID,设备号搜索)
|
||||
- 使用 `ArtTable` 展示设备列表
|
||||
- 使用 `ArtTableHeader` 添加"授权设备"和"撤销授权"按钮
|
||||
- **验证**: 页面能正常渲染,无控制台错误
|
||||
|
||||
9. **实现设备列表查询功能**
|
||||
- 实现 `loadDeviceList()` 方法调用 API
|
||||
- 实现搜索和重置功能
|
||||
- 实现分页功能
|
||||
- 添加 loading 状态
|
||||
- **验证**: 能正确展示设备列表数据,分页工作正常
|
||||
|
||||
10. **实现授权设备对话框**
|
||||
- 创建授权设备对话框
|
||||
- 使用 `ElForm` + `ElInput` (textarea) 输入设备号列表
|
||||
- 支持多行输入或逗号分隔
|
||||
- 添加备注输入框 (可选)
|
||||
- 实现表单验证 (必填项,格式校验)
|
||||
- **验证**: 对话框显示正常,表单验证工作
|
||||
|
||||
11. **实现授权设备提交逻辑**
|
||||
- 实现 `handleAllocateDevices()` 方法
|
||||
- 解析设备号列表 (处理换行和逗号分隔)
|
||||
- 调用 `EnterpriseService.allocateDevices()` API
|
||||
- 展示授权结果 (成功数量,失败数量,失败列表)
|
||||
- 使用 `ElMessageBox` 或 `ElDialog` 展示详细结果
|
||||
- 授权成功后刷新列表
|
||||
- **验证**: 能成功授权设备,正确处理部分成功/失败情况
|
||||
|
||||
12. **实现撤销授权对话框**
|
||||
- 创建撤销授权对话框
|
||||
- 使用表格多选模式选择要撤销的设备
|
||||
- 或者使用输入框输入设备号列表
|
||||
- 实现二次确认逻辑
|
||||
- **验证**: 对话框显示正常
|
||||
|
||||
13. **实现撤销授权提交逻辑**
|
||||
- 实现 `handleRecallDevices()` 方法
|
||||
- 收集选中的设备号
|
||||
- 调用 `EnterpriseService.recallDevices()` API
|
||||
- 展示撤销结果 (成功数量,失败数量,失败列表)
|
||||
- 撤销成功后刷新列表
|
||||
- **验证**: 能成功撤销授权,正确处理部分成功/失败情况
|
||||
|
||||
### Phase 6: Polish & Testing
|
||||
|
||||
14. **完善表格列配置**
|
||||
- 配置表格列 (设备ID,设备号,设备名称,设备型号,绑定卡数量,授权时间)
|
||||
- 实现列显示/隐藏功能
|
||||
- 添加时间格式化
|
||||
- **验证**: 表格数据展示完整美观
|
||||
|
||||
15. **添加错误处理**
|
||||
- 为所有 API 调用添加 try-catch
|
||||
- 添加友好的错误提示消息
|
||||
- 处理网络错误和业务错误
|
||||
- **验证**: 各种错误场景都有适当提示
|
||||
|
||||
16. **样式调整**
|
||||
- 确保页面样式与系统其他页面一致
|
||||
- 响应式布局适配
|
||||
- 对话框尺寸和布局优化
|
||||
- **验证**: 在不同屏幕尺寸下显示正常
|
||||
|
||||
17. **最终验证**
|
||||
- 运行 `npm run type-check` 确保无类型错误
|
||||
- 运行 `npm run build` 确保能成功构建
|
||||
- 运行 `openspec validate add-enterprise-device-authorization --strict` 验证 spec
|
||||
- 手动测试所有功能点
|
||||
- 测试中英文切换
|
||||
- **验证**: 所有功能正常,无控制台错误
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Task 3 依赖 Task 1 (API 需要类型定义)
|
||||
- Task 8-13 依赖 Task 1-7 (UI 需要 API 和路由)
|
||||
- Task 14-16 是并行任务,可以同时进行
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- Phase 1-2: 30 分钟 (类型和 API)
|
||||
- Phase 3: 30 分钟 (国际化)
|
||||
- Phase 4: 15 分钟 (路由)
|
||||
- Phase 5: 2-3 小时 (UI 实现)
|
||||
- Phase 6: 30 分钟 (测试和优化)
|
||||
|
||||
**Total**: 约 4-4.5 小时
|
||||
43
openspec/changes/add-order-management/proposal.md
Normal file
43
openspec/changes/add-order-management/proposal.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Change: Add Order Management System
|
||||
|
||||
## Why
|
||||
|
||||
The IoT management platform currently lacks order management capabilities. Users need to:
|
||||
- View and query orders created by customers (personal and agent)
|
||||
- Track order payment status and details
|
||||
- Create orders for single card or device purchases
|
||||
- Cancel pending orders
|
||||
- View order history with filtering and search
|
||||
|
||||
This capability is essential for financial tracking, commission calculation, and overall business operations transparency.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **NEW**: Order list page with search, filtering, and pagination
|
||||
- **NEW**: Order API service with full CRUD operations
|
||||
- **NEW**: TypeScript types for order entities and API contracts
|
||||
- **NEW**: i18n keys for order management UI
|
||||
- **NEW**: Router configuration for order management module
|
||||
|
||||
The order management module will support:
|
||||
- Listing orders with filters (payment status, order type, date range, order number)
|
||||
- Viewing order details including buyer information, order items (packages), and payment details
|
||||
- Creating orders for single card or device purchases with package selection
|
||||
- Canceling orders that are in pending payment status
|
||||
- Displaying payment method, commission status, and order totals
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: `order-management` (new capability)
|
||||
- **Affected code**:
|
||||
- `src/types/api/order.ts` (new file - TypeScript types)
|
||||
- `src/api/modules/order.ts` (new file - API service)
|
||||
- `src/views/order-management/order-list/index.vue` (new file - order list page)
|
||||
- `src/router/routes/asyncRoutes.ts` (route configuration)
|
||||
- `src/router/routesAlias.ts` (route alias)
|
||||
- `src/locales/langs/zh.json` (Chinese i18n)
|
||||
- `src/locales/langs/en.json` (English i18n)
|
||||
- `src/types/api/index.ts` (exports)
|
||||
- `src/api/modules/index.ts` (exports)
|
||||
- **Dependencies**: Requires backend APIs at `/api/admin/orders` endpoints
|
||||
- **Breaking changes**: None (this is a new feature)
|
||||
@@ -0,0 +1,174 @@
|
||||
# Order Management Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Order List Display
|
||||
|
||||
The system SHALL display a paginated list of orders with comprehensive filtering and search capabilities.
|
||||
|
||||
#### Scenario: Display all orders with pagination
|
||||
|
||||
- **GIVEN** the user navigates to the order management page
|
||||
- **WHEN** the page loads
|
||||
- **THEN** the system displays a table of orders with pagination controls
|
||||
- **AND** default page size is 20 items
|
||||
- **AND** table shows columns: ID, order number, buyer type, buyer ID, order type, payment status, total amount, created date, and actions
|
||||
|
||||
#### Scenario: Filter orders by payment status
|
||||
|
||||
- **GIVEN** the user is on the order list page
|
||||
- **WHEN** the user selects a payment status filter (pending=1, paid=2, cancelled=3, refunded=4)
|
||||
- **AND** clicks the search button
|
||||
- **THEN** the system displays only orders matching the selected payment status
|
||||
- **AND** pagination resets to page 1
|
||||
|
||||
#### Scenario: Filter orders by order type
|
||||
|
||||
- **GIVEN** the user is on the order list page
|
||||
- **WHEN** the user selects an order type filter (single_card or device)
|
||||
- **AND** clicks the search button
|
||||
- **THEN** the system displays only orders matching the selected order type
|
||||
|
||||
#### Scenario: Search by order number
|
||||
|
||||
- **GIVEN** the user is on the order list page
|
||||
- **WHEN** the user enters an order number in the search field
|
||||
- **AND** clicks the search button
|
||||
- **THEN** the system performs an exact match search
|
||||
- **AND** displays the matching order if found
|
||||
|
||||
#### Scenario: Filter by date range
|
||||
|
||||
- **GIVEN** the user is on the order list page
|
||||
- **WHEN** the user selects a start date and/or end date
|
||||
- **AND** clicks the search button
|
||||
- **THEN** the system displays orders created within the specified date range
|
||||
|
||||
### Requirement: Order Details Viewing
|
||||
|
||||
The system SHALL allow users to view detailed information about each order including order items and payment information.
|
||||
|
||||
#### Scenario: View order details
|
||||
|
||||
- **GIVEN** the user is viewing the order list
|
||||
- **WHEN** the user clicks the view/detail action for an order
|
||||
- **THEN** the system displays comprehensive order information including:
|
||||
- **AND** order basic info (order_no, order_type, buyer_id, buyer_type)
|
||||
- **AND** payment info (payment_status, payment_method, paid_at, total_amount)
|
||||
- **AND** order items list (package_id, package_name, quantity, unit_price, amount)
|
||||
- **AND** commission info (commission_status, commission_config_version)
|
||||
- **AND** timestamps (created_at, updated_at)
|
||||
|
||||
### Requirement: Order Cancellation
|
||||
|
||||
The system SHALL allow authorized users to cancel orders that are in pending payment status.
|
||||
|
||||
#### Scenario: Cancel a pending order
|
||||
|
||||
- **GIVEN** the user is viewing an order with payment_status = 1 (pending)
|
||||
- **WHEN** the user clicks the cancel action
|
||||
- **AND** confirms the cancellation in the confirmation dialog
|
||||
- **THEN** the system sends a cancel request to POST `/api/admin/orders/{id}/cancel`
|
||||
- **AND** updates the order status to cancelled (3)
|
||||
- **AND** displays a success message
|
||||
- **AND** refreshes the order list
|
||||
|
||||
#### Scenario: Cannot cancel paid order
|
||||
|
||||
- **GIVEN** the user is viewing an order with payment_status = 2 (paid)
|
||||
- **WHEN** the cancel action is clicked
|
||||
- **THEN** the system displays an error message "Cannot cancel a paid order"
|
||||
- **AND** does not send the cancellation request
|
||||
|
||||
### Requirement: Order Creation
|
||||
|
||||
The system SHALL provide an interface to create orders for single card or device purchases.
|
||||
|
||||
#### Scenario: Create single card order
|
||||
|
||||
- **GIVEN** the user clicks the create order button
|
||||
- **WHEN** the user selects order_type = "single_card"
|
||||
- **AND** selects an IoT card (iot_card_id)
|
||||
- **AND** selects one or more packages (package_ids)
|
||||
- **AND** submits the form
|
||||
- **THEN** the system sends a POST request to `/api/admin/orders` with the order data
|
||||
- **AND** displays the newly created order details
|
||||
- **AND** refreshes the order list
|
||||
|
||||
#### Scenario: Create device order
|
||||
|
||||
- **GIVEN** the user clicks the create order button
|
||||
- **WHEN** the user selects order_type = "device"
|
||||
- **AND** selects a device (device_id)
|
||||
- **AND** selects one or more packages (package_ids, max 10)
|
||||
- **AND** submits the form
|
||||
- **THEN** the system creates the order
|
||||
- **AND** displays a success message
|
||||
|
||||
### Requirement: Order Status Display
|
||||
|
||||
The system SHALL display order payment status and order type using color-coded badges and human-readable text.
|
||||
|
||||
#### Scenario: Display payment status badge
|
||||
|
||||
- **GIVEN** an order is displayed in the table
|
||||
- **WHEN** the payment_status is 1 (pending)
|
||||
- **THEN** the system displays a warning-type badge with text "待支付"
|
||||
- **WHEN** the payment_status is 2 (paid)
|
||||
- **THEN** the system displays a success-type badge with text "已支付"
|
||||
- **WHEN** the payment_status is 3 (cancelled)
|
||||
- **THEN** the system displays an info-type badge with text "已取消"
|
||||
- **WHEN** the payment_status is 4 (refunded)
|
||||
- **THEN** the system displays a danger-type badge with text "已退款"
|
||||
|
||||
#### Scenario: Display order type badge
|
||||
|
||||
- **GIVEN** an order is displayed in the table
|
||||
- **WHEN** the order_type is "single_card"
|
||||
- **THEN** the system displays text "单卡购买"
|
||||
- **WHEN** the order_type is "device"
|
||||
- **THEN** the system displays text "设备购买"
|
||||
|
||||
### Requirement: Currency Formatting
|
||||
|
||||
The system SHALL display monetary amounts in yuan (元) with proper formatting and conversion from cents.
|
||||
|
||||
#### Scenario: Format order total amount
|
||||
|
||||
- **GIVEN** an order has total_amount = 50000 (in cents)
|
||||
- **WHEN** the order is displayed in the table
|
||||
- **THEN** the system displays "¥500.00" or "500.00 元"
|
||||
|
||||
### Requirement: Data Refresh and Real-time Updates
|
||||
|
||||
The system SHALL provide manual refresh capabilities and update data after mutations.
|
||||
|
||||
#### Scenario: Manual refresh
|
||||
|
||||
- **GIVEN** the user is viewing the order list
|
||||
- **WHEN** the user clicks the refresh button in the table header
|
||||
- **THEN** the system reloads the current page of orders with current filters
|
||||
- **AND** maintains the current pagination state
|
||||
|
||||
#### Scenario: Auto-refresh after order cancellation
|
||||
|
||||
- **GIVEN** the user successfully cancels an order
|
||||
- **WHEN** the cancellation is confirmed
|
||||
- **THEN** the system automatically refreshes the order list
|
||||
- **AND** displays the updated order status
|
||||
|
||||
### Requirement: Internationalization Support
|
||||
|
||||
The system SHALL provide full internationalization support for order management UI in Chinese and English.
|
||||
|
||||
#### Scenario: Display Chinese text
|
||||
|
||||
- **GIVEN** the user's language is set to Chinese (zh)
|
||||
- **WHEN** the order management page is viewed
|
||||
- **THEN** all UI text displays in Chinese including menu titles, table headers, status labels, and messages
|
||||
|
||||
#### Scenario: Display English text
|
||||
|
||||
- **GIVEN** the user's language is set to English (en)
|
||||
- **WHEN** the order management page is viewed
|
||||
- **THEN** all UI text displays in English including menu titles, table headers, status labels, and messages
|
||||
57
openspec/changes/add-order-management/tasks.md
Normal file
57
openspec/changes/add-order-management/tasks.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Type Definitions
|
||||
|
||||
- [ ] 1.1 Create `src/types/api/order.ts` with order types, enums, and interfaces
|
||||
- [ ] 1.2 Add order type exports to `src/types/api/index.ts`
|
||||
|
||||
## 2. API Service Layer
|
||||
|
||||
- [ ] 2.1 Create `src/api/modules/order.ts` with OrderService class
|
||||
- [ ] 2.2 Implement `getOrders()` - list orders with pagination and filters
|
||||
- [ ] 2.3 Implement `getOrderById()` - fetch single order details
|
||||
- [ ] 2.4 Implement `createOrder()` - create new order
|
||||
- [ ] 2.5 Implement `cancelOrder()` - cancel pending order
|
||||
- [ ] 2.6 Add OrderService export to `src/api/modules/index.ts`
|
||||
|
||||
## 3. Internationalization
|
||||
|
||||
- [ ] 3.1 Add Chinese translations to `src/locales/langs/zh.json` under `orderManagement` namespace
|
||||
- [ ] 3.2 Add English translations to `src/locales/langs/en.json` under `orderManagement` namespace
|
||||
- [ ] 3.3 Include keys for: menu titles, page titles, table columns, search fields, statuses, actions, messages
|
||||
|
||||
## 4. Routing Configuration
|
||||
|
||||
- [ ] 4.1 Add `OrderList` route alias to `src/router/routesAlias.ts`
|
||||
- [ ] 4.2 Add order management route group to `src/router/routes/asyncRoutes.ts`
|
||||
- [ ] 4.3 Configure route with proper icon, title, and keepAlive settings
|
||||
|
||||
## 5. UI Components
|
||||
|
||||
- [ ] 5.1 Create `src/views/order-management/order-list/index.vue` component skeleton
|
||||
- [ ] 5.2 Implement search bar with filters (order_no, payment_status, order_type, date range)
|
||||
- [ ] 5.3 Implement data table with columns (ID, order_no, buyer info, order type, payment status, total amount, created_at, actions)
|
||||
- [ ] 5.4 Add pagination controls
|
||||
- [ ] 5.5 Implement view details action (navigate to detail view or show in dialog)
|
||||
- [ ] 5.6 Implement cancel order action with confirmation dialog
|
||||
- [ ] 5.7 Add status badges and formatters for payment status and order type
|
||||
- [ ] 5.8 Format currency amounts (分 to 元 conversion)
|
||||
- [ ] 5.9 Implement create order button and dialog (optional - can be phase 2)
|
||||
|
||||
## 6. Business Logic
|
||||
|
||||
- [ ] 6.1 Implement order list data fetching with loading states
|
||||
- [ ] 6.2 Implement search and filter logic
|
||||
- [ ] 6.3 Implement pagination handlers
|
||||
- [ ] 6.4 Implement cancel order with optimistic UI updates
|
||||
- [ ] 6.5 Add error handling and user feedback (ElMessage)
|
||||
- [ ] 6.6 Implement date/time formatting using project utilities
|
||||
|
||||
## 7. Validation & Polish
|
||||
|
||||
- [ ] 7.1 Test search and filtering functionality
|
||||
- [ ] 7.2 Test pagination and data refresh
|
||||
- [ ] 7.3 Test cancel order flow
|
||||
- [ ] 7.4 Verify i18n coverage (switch language and check all text)
|
||||
- [ ] 7.5 Verify responsive layout and table column configuration
|
||||
- [ ] 7.6 Code review and cleanup (remove console logs, verify TypeScript types)
|
||||
332
openspec/changes/add-package-management-system/design.md
Normal file
332
openspec/changes/add-package-management-system/design.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Design Document: 套餐管理系统实现
|
||||
|
||||
## Context
|
||||
|
||||
实现完整的套餐管理系统,包括4个核心模块。该系统需要支持多级代理商体系的套餐分配和定价管理。
|
||||
|
||||
**背景**:
|
||||
- 项目已有类型定义(src/types/api/package.ts),但使用不同的字段命名和枚举值
|
||||
- 后端 API 已实现,使用下划线命名(如 `series_name`)
|
||||
- 前端项目统一使用 CommonStatus 枚举(0:禁用, 1:启用)
|
||||
- 参考实现:`/system/role` 页面使用了组件化架构
|
||||
|
||||
**约束**:
|
||||
- 必须保留现有类型定义文件,不能破坏现有代码
|
||||
- 需要兼容后端 API 的字段命名规范
|
||||
- 需要适配项目的状态枚举规范
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
1. 实现4个核心模块的完整 CRUD 功能
|
||||
2. 建立统一的 API 服务层,封装后端接口
|
||||
3. 实现组件化的页面结构,参考 `/system/role`
|
||||
4. 支持复杂的定价规则(系列加价 vs 单套餐覆盖)
|
||||
5. 确保数据隔离和权限控制
|
||||
|
||||
### Non-Goals
|
||||
1. 不重构现有的 package.ts 类型定义
|
||||
2. 不实现套餐的实时统计和报表功能(后续迭代)
|
||||
3. 不实现套餐批量导入功能(后续迭代)
|
||||
4. 不实现套餐的版本管理功能
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: API 字段命名策略
|
||||
|
||||
**问题**:后端使用下划线命名(snake_case),前端类型通常使用驼峰命名(camelCase)。
|
||||
|
||||
**决策**:
|
||||
- API 请求/响应保持下划线命名,与后端保持一致
|
||||
- 创建新的类型文件 `packageManagement.ts`,使用下划线命名
|
||||
- 在表单提交和响应处理时不做转换,直接使用下划线字段
|
||||
|
||||
**理由**:
|
||||
- 减少转换层的复杂性和错误风险
|
||||
- 与后端 API 文档保持一致,便于对照
|
||||
- TypeScript 支持下划线字段名,不影响类型安全
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
export interface PackageSeriesResponse {
|
||||
id: number
|
||||
series_code: string // 下划线命名
|
||||
series_name: string
|
||||
status: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
```
|
||||
|
||||
### Decision 2: 状态值映射
|
||||
|
||||
**问题**:文档中状态是 `1:启用, 2:禁用`,但项目 CommonStatus 是 `0:禁用, 1:启用`。
|
||||
|
||||
**决策**:
|
||||
- **在常量配置中定义套餐专用的状态枚举**
|
||||
- **前端页面使用项目统一的 CommonStatus(0/1)**
|
||||
- **在 API 服务层进行状态值映射转换**
|
||||
|
||||
**映射规则**:
|
||||
```typescript
|
||||
// 前端 -> 后端
|
||||
CommonStatus.ENABLED (1) -> API Status (1)
|
||||
CommonStatus.DISABLED (0) -> API Status (2)
|
||||
|
||||
// 后端 -> 前端
|
||||
API Status (1) -> CommonStatus.ENABLED (1)
|
||||
API Status (2) -> CommonStatus.DISABLED (0)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 保持前端 UI 的一致性
|
||||
- 避免混淆项目开发者
|
||||
- 集中在 API 服务层处理差异
|
||||
|
||||
### Decision 3: 模块拆分策略
|
||||
|
||||
**问题**:是创建单个 package.ts 服务,还是拆分为多个服务文件?
|
||||
|
||||
**决策**:拆分为4个独立的服务文件:
|
||||
1. `packageSeries.ts` - 套餐系列管理
|
||||
2. `package.ts` - 套餐管理
|
||||
3. `myPackage.ts` - 代理可售套餐
|
||||
4. `shopPackageAllocation.ts` - 单套餐分配
|
||||
|
||||
**理由**:
|
||||
- 每个模块功能独立,职责清晰
|
||||
- 便于维护和扩展
|
||||
- 符合单一职责原则
|
||||
- 便于团队协作(不同开发者负责不同模块)
|
||||
|
||||
**替代方案**:
|
||||
- 单个 package.ts 文件 - **拒绝**,文件过大,难以维护
|
||||
|
||||
### Decision 4: 定价规则实现
|
||||
|
||||
**问题**:代理商的套餐成本价有两种计算方式:系列加价和单套餐覆盖。
|
||||
|
||||
**决策**:
|
||||
- **后端负责成本价计算**,前端只展示结果
|
||||
- 前端接收 `price_source` 字段,标识价格来源
|
||||
- 单套餐分配创建时,保存 `calculated_cost_price`(系列规则计算的价格)供参考
|
||||
|
||||
**数据流**:
|
||||
```
|
||||
1. 系列分配:pricing_mode + pricing_value -> 后端计算 -> cost_price
|
||||
2. 单套餐分配:直接设置 cost_price(覆盖系列规则)
|
||||
3. 前端展示:price_source 标识使用了哪种规则
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 计算逻辑复杂,集中在后端便于维护
|
||||
- 前端只负责展示,降低复杂度
|
||||
- 保留 calculated_cost_price 便于调试和审计
|
||||
|
||||
### Decision 5: 表单验证策略
|
||||
|
||||
**问题**:客户端验证 vs 服务端验证。
|
||||
|
||||
**决策**:**双重验证**
|
||||
- 客户端:使用 Element Plus 的 FormRules 进行基础验证
|
||||
- 服务端:后端 API 进行完整验证并返回详细错误
|
||||
|
||||
**客户端验证规则**:
|
||||
- 必填字段检查
|
||||
- 长度限制(如系列名称 1-255 字符)
|
||||
- 数值范围(如套餐时长 1-120 月)
|
||||
- 格式验证(如价格必须为正整数)
|
||||
|
||||
**理由**:
|
||||
- 客户端验证提升用户体验,即时反馈
|
||||
- 服务端验证保证数据安全性和完整性
|
||||
- 符合 Web 应用最佳实践
|
||||
|
||||
### Decision 6: 页面组件化结构
|
||||
|
||||
**问题**:页面结构如何组织?
|
||||
|
||||
**决策**:参考 `/system/role` 页面,使用组件化结构:
|
||||
```vue
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<ArtSearchBar /> <!-- 搜索栏 -->
|
||||
<ElCard>
|
||||
<ArtTableHeader /> <!-- 表格头部:刷新、列设置、操作按钮 -->
|
||||
<ArtTable /> <!-- 数据表格 -->
|
||||
<ElDialog /> <!-- 新增/编辑对话框 -->
|
||||
</ElCard>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 与项目现有页面风格一致
|
||||
- 复用成熟的组件,减少开发工作量
|
||||
- 便于维护和扩展
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: 后端 API 未完成
|
||||
|
||||
**风险**:后端接口可能尚未实现或与文档不一致。
|
||||
|
||||
**缓解措施**:
|
||||
1. 先实现 API 服务层,使用 TypeScript 类型约束
|
||||
2. 使用 Mock 数据进行前端开发(已有示例)
|
||||
3. 与后端团队确认 API 规范和联调时间
|
||||
4. 预留 API 调试和修正时间
|
||||
|
||||
### Risk 2: 状态值映射可能遗漏
|
||||
|
||||
**风险**:在某些地方忘记转换状态值,导致显示错误。
|
||||
|
||||
**缓解措施**:
|
||||
1. 在 API 服务层统一处理转换
|
||||
2. 创建工具函数封装映射逻辑
|
||||
3. 编写单元测试覆盖映射函数
|
||||
4. Code Review 时重点检查状态相关代码
|
||||
|
||||
### Risk 3: 定价规则理解偏差
|
||||
|
||||
**风险**:对定价规则的理解与实际业务需求有偏差。
|
||||
|
||||
**缓解措施**:
|
||||
1. 在实现前与产品确认定价规则
|
||||
2. 编写测试用例覆盖各种定价场景
|
||||
3. 在 UI 上清晰展示价格来源和计算方式
|
||||
4. 预留调整空间,避免硬编码
|
||||
|
||||
### Trade-off 1: 类型定义冗余
|
||||
|
||||
**取舍**:保留旧的 package.ts 类型定义,新增 packageManagement.ts。
|
||||
|
||||
**代价**:
|
||||
- 存在两套类型定义,可能造成混淆
|
||||
- 占用额外的代码空间
|
||||
|
||||
**收益**:
|
||||
- 不影响现有代码,向后兼容
|
||||
- 新旧系统可以并存,降低迁移风险
|
||||
- 未来可以逐步迁移到新类型
|
||||
|
||||
### Trade-off 2: 状态值映射增加复杂度
|
||||
|
||||
**取舍**:在 API 服务层进行状态值转换。
|
||||
|
||||
**代价**:
|
||||
- 增加一层转换逻辑
|
||||
- 可能影响性能(微小)
|
||||
|
||||
**收益**:
|
||||
- 前端 UI 保持一致性
|
||||
- 业务逻辑更清晰
|
||||
- 便于后续维护
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: 基础设施(1-2天)
|
||||
1. 创建类型定义文件
|
||||
2. 创建常量配置文件
|
||||
3. 设置状态映射工具函数
|
||||
|
||||
### Phase 2: API 服务层(2-3天)
|
||||
1. 实现4个 API 服务模块
|
||||
2. 编写单元测试(可选)
|
||||
3. 使用 Mock 数据测试
|
||||
|
||||
### Phase 3: 页面实现(4-5天)
|
||||
1. 套餐系列管理页面(1天)
|
||||
2. 套餐管理页面(1.5天)
|
||||
3. 代理可售套餐页面(1天)
|
||||
4. 单套餐分配页面(1.5天)
|
||||
|
||||
### Phase 4: 集成测试(1-2天)
|
||||
1. 与后端 API 联调
|
||||
2. 端到端功能测试
|
||||
3. 修复 Bug 和优化
|
||||
|
||||
### Phase 5: 上线(1天)
|
||||
1. Code Review
|
||||
2. 合并代码
|
||||
3. 部署到测试环境
|
||||
4. 部署到生产环境
|
||||
|
||||
**总计**:9-13 个工作日
|
||||
|
||||
### Rollback Plan
|
||||
|
||||
如果出现严重问题,回滚步骤:
|
||||
1. 从 Git 回滚到上一个稳定版本
|
||||
2. 移除新增的路由配置
|
||||
3. 移除新增的 API 服务导出
|
||||
4. 通知用户功能暂时不可用
|
||||
|
||||
### Decision 7: 错误处理策略
|
||||
|
||||
**问题**:如何统一处理各类错误和异常?
|
||||
|
||||
**决策**:分层错误处理机制
|
||||
- **网络错误**:axios 拦截器统一捕获,显示通用错误提示
|
||||
- **401 未认证**:自动跳转到登录页面
|
||||
- **403 无权限**:显示权限不足提示,不跳转
|
||||
- **400 业务错误**:根据错误信息显示具体提示(ElMessage.error)
|
||||
- **表单验证错误**:在表单字段下显示错误提示
|
||||
|
||||
**错误提示方式**:
|
||||
```typescript
|
||||
// 网络错误或服务器错误
|
||||
ElMessage.error('网络错误,请稍后重试')
|
||||
|
||||
// 业务错误(后端返回的具体错误)
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
|
||||
// 操作成功
|
||||
ElMessage.success('操作成功')
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 统一的错误处理提升用户体验
|
||||
- 分层处理避免重复代码
|
||||
- 清晰的错误提示帮助用户理解问题
|
||||
|
||||
### Decision 8: Loading 状态管理
|
||||
|
||||
**问题**:如何管理各种操作的加载状态?
|
||||
|
||||
**决策**:细粒度的 loading 状态管理
|
||||
|
||||
**Loading 状态分类**:
|
||||
```typescript
|
||||
const loading = ref(false) // 表格数据加载
|
||||
const submitLoading = ref(false) // 表单提交
|
||||
const deleteLoading = ref<Record<number, boolean>>({}) // 删除操作(可选)
|
||||
```
|
||||
|
||||
**状态管理规则**:
|
||||
- **列表查询**:表格显示 loading 遮罩
|
||||
- **新增/编辑提交**:提交按钮显示 loading,禁用表单
|
||||
- **删除操作**:可选择在按钮上显示 loading 或全局 loading
|
||||
- **状态切换**:ElSwitch 自带 loading 效果,先更新 UI 再调用 API
|
||||
|
||||
**理由**:
|
||||
- 细粒度控制提供更好的交互反馈
|
||||
- 防止重复提交
|
||||
- 清晰标识正在进行的操作
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Q**: 套餐被删除后,历史订单如何处理?
|
||||
**A**: 待产品确认,可能需要软删除机制
|
||||
|
||||
2. **Q**: 代理商可以自行调整套餐售价吗?
|
||||
**A**: 待产品确认,当前设计只展示建议售价
|
||||
|
||||
3. **Q**: 套餐系列和套餐是否支持批量操作(批量启用/禁用)?
|
||||
**A**: 当前不支持,后续迭代考虑
|
||||
|
||||
4. **Q**: 是否需要套餐变更历史记录?
|
||||
**A**: 后端可能有审计日志,前端暂不展示
|
||||
|
||||
5. **Q**: 单套餐分配的"原计算成本价"是否需要实时更新?
|
||||
**A**: 待确认,当前设计是创建时计算一次,不自动更新
|
||||
143
openspec/changes/add-package-management-system/proposal.md
Normal file
143
openspec/changes/add-package-management-system/proposal.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Change: 套餐管理系统实现
|
||||
|
||||
## Why
|
||||
|
||||
根据业务需求文档(docs/套餐.md),需要实现完整的套餐管理系统,包括4个核心模块:
|
||||
|
||||
1. **套餐系列管理** - 管理套餐的分类和组织
|
||||
2. **套餐管理** - 管理具体的套餐产品(流量、价格、时长等)
|
||||
3. **代理可售套餐** - 代理商查看被分配的可售套餐及定价
|
||||
4. **单套餐分配** - 为代理商分配特定套餐并设置成本价
|
||||
|
||||
当前系统虽然有套餐相关的类型定义(src/types/api/package.ts)和部分页面骨架,但缺少完整的 API 对接和业务逻辑实现。此变更将实现完整的套餐管理能力。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 1. API 层实现
|
||||
|
||||
采用模块化设计,拆分为 4 个独立的 API 服务文件:
|
||||
|
||||
- **新增**: `src/api/modules/packageSeries.ts` - 套餐系列 API 服务
|
||||
- 套餐系列列表查询(分页、筛选)
|
||||
- 创建套餐系列
|
||||
- 获取套餐系列详情
|
||||
- 更新套餐系列
|
||||
- 删除套餐系列
|
||||
- 更新套餐系列状态
|
||||
|
||||
- **新增**: `src/api/modules/package.ts` - 套餐管理 API 服务
|
||||
- 套餐列表查询(分页、多条件筛选)
|
||||
- 创建套餐
|
||||
- 获取套餐详情
|
||||
- 更新套餐
|
||||
- 删除套餐
|
||||
- 更新套餐状态
|
||||
- 更新套餐上架状态
|
||||
- 获取系列下拉选项(用于表单选择)
|
||||
|
||||
- **新增**: `src/api/modules/myPackage.ts` - 代理可售套餐 API 服务
|
||||
- 我的可售套餐列表查询
|
||||
- 获取可售套餐详情
|
||||
- 我的被分配系列列表
|
||||
|
||||
- **新增**: `src/api/modules/shopPackageAllocation.ts` - 单套餐分配 API 服务
|
||||
- 单套餐分配列表查询
|
||||
- 创建单套餐分配
|
||||
- 获取单套餐分配详情
|
||||
- 更新单套餐分配
|
||||
- 删除单套餐分配
|
||||
- 更新单套餐分配状态
|
||||
|
||||
- **修改**: `src/api/modules/index.ts` - 导出新增的服务模块
|
||||
- 导出 PackageSeriesService
|
||||
- 导出 PackageService
|
||||
- 导出 MyPackageService
|
||||
- 导出 ShopPackageAllocationService
|
||||
|
||||
### 2. 类型定义增强
|
||||
|
||||
- **新增**: `src/types/api/packageManagement.ts` - 完整的套餐管理类型定义
|
||||
- 匹配文档的 API 字段(下划线命名)
|
||||
- 包含所有请求/响应类型
|
||||
- 包含分页结果类型
|
||||
|
||||
### 3. 页面实现
|
||||
|
||||
**套餐系列管理** (`src/views/package-management/package-series/index.vue`)
|
||||
- 列表展示(支持名称搜索、状态筛选)
|
||||
- 新增/编辑套餐系列
|
||||
- 删除套餐系列
|
||||
- 状态开关(启用/禁用)
|
||||
|
||||
**套餐管理** (`src/views/package-management/package-list/index.vue`)
|
||||
- 列表展示(支持多条件筛选:名称、系列、状态、上架状态、套餐类型)
|
||||
- 新增/编辑套餐
|
||||
- 删除套餐
|
||||
- 状态开关(启用/禁用)
|
||||
- 上架状态开关(上架/下架)
|
||||
|
||||
**代理可售套餐** (`src/views/package-management/my-packages/index.vue`)
|
||||
- 查看被分配的套餐列表(支持系列、类型筛选)
|
||||
- 查看套餐详情(成本价、建议售价、利润空间等)
|
||||
- 查看被分配系列列表
|
||||
|
||||
**单套餐分配** (`src/views/package-management/package-assign/index.vue`)
|
||||
- 分配列表(支持店铺、套餐、状态筛选)
|
||||
- 创建分配(选择套餐、店铺、设置成本价)
|
||||
- 编辑分配(修改成本价)
|
||||
- 删除分配
|
||||
- 状态管理(启用/禁用)
|
||||
|
||||
### 4. 常量配置
|
||||
|
||||
- **新增**: `src/config/constants/package.ts` - 套餐相关常量
|
||||
- 套餐类型枚举(formal/addon)
|
||||
- 流量类型枚举(real/virtual)
|
||||
- 上架状态枚举(1:上架, 2:下架)
|
||||
- 定价模式枚举(fixed/percent)
|
||||
- 价格来源枚举(series_pricing/package_override)
|
||||
|
||||
### 5. 路由配置
|
||||
|
||||
已存在的路由(无需修改):
|
||||
- `/package-management/package-series` - 套餐系列管理
|
||||
- `/package-management/package-list` - 套餐管理
|
||||
- `/package-management/package-assign` - 单套餐分配
|
||||
|
||||
需要新增的路由:
|
||||
- **新增**: `src/router/routesAlias.ts` - 添加路由别名
|
||||
- `MyPackages = '/package-management/my-packages'` - 代理可售套餐
|
||||
|
||||
- **新增**: `src/router/routes/asyncRoutes.ts` - 添加路由配置
|
||||
- `/package-management/my-packages` - 代理可售套餐页面路由
|
||||
|
||||
## Impact
|
||||
|
||||
### 受影响的规范
|
||||
- `package-series-management` - 新增能力
|
||||
- `package-management` - 新增能力
|
||||
- `my-packages` - 新增能力
|
||||
- `shop-package-allocation` - 新增能力
|
||||
|
||||
### 受影响的代码
|
||||
- `src/api/modules/*` - 新增 4 个 API 服务模块
|
||||
- `src/types/api/*` - 新增类型定义文件
|
||||
- `src/views/package-management/*` - 4 个页面完整实现
|
||||
- `src/config/constants/*` - 新增常量配置
|
||||
- `src/router/routes/asyncRoutes.ts` - 路由配置
|
||||
|
||||
### 依赖关系
|
||||
- 依赖现有的组件库(ArtTable、ArtSearchBar、ArtTableHeader 等)
|
||||
- 依赖现有的 HTTP 请求工具(request.ts)
|
||||
- 依赖现有的权限控制和路由守卫
|
||||
- 依赖 ShopService(用于单套餐分配页面的店铺选择器)
|
||||
- 后端 API 需已实现(docs/套餐.md 中定义的接口)
|
||||
|
||||
**注意事项**:
|
||||
- ShopService 应该已经存在于 src/api/modules/shop.ts
|
||||
- 如果不存在,需要先实现或使用 Mock 数据
|
||||
|
||||
### 风险评估
|
||||
- **低风险**: 独立模块,不影响现有功能
|
||||
- **API 依赖**: 需确保后端接口已实现并联调
|
||||
- **权限控制**: 需配置对应的菜单和按钮权限
|
||||
@@ -0,0 +1,110 @@
|
||||
# My Packages (代理可售套餐) Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 我的可售套餐列表查询
|
||||
|
||||
系统 SHALL 提供代理商查询被分配套餐的功能。
|
||||
|
||||
#### Scenario: 查询当前代理商的可售套餐
|
||||
|
||||
- **WHEN** 代理商用户访问可售套餐页面
|
||||
- **THEN** 系统显示被分配给该代理商的套餐列表
|
||||
- **AND** 每个套餐包含:套餐ID、套餐编码、套餐名称、套餐类型、系列信息
|
||||
- **AND** 显示成本价(cost_price,单位:分)
|
||||
- **AND** 显示建议售价(suggested_retail_price,单位:分)
|
||||
- **AND** 显示利润空间(profit_margin = 建议售价 - 成本价)
|
||||
- **AND** 显示价格来源(series_pricing:系列加价 / package_override:单套餐覆盖)
|
||||
- **AND** 显示套餐状态和上架状态
|
||||
- **AND** 支持分页,每页最多100条记录
|
||||
|
||||
#### Scenario: 按系列筛选
|
||||
|
||||
- **WHEN** 代理商选择特定系列ID筛选
|
||||
- **THEN** 系统返回该系列下的所有可售套餐
|
||||
|
||||
#### Scenario: 按套餐类型筛选
|
||||
|
||||
- **WHEN** 代理商选择套餐类型(formal/addon)筛选
|
||||
- **THEN** 系统返回该类型的所有可售套餐
|
||||
|
||||
### Requirement: 可售套餐详情查询
|
||||
|
||||
系统 SHALL 允许代理商查看单个可售套餐的详细信息。
|
||||
|
||||
#### Scenario: 查询套餐详情
|
||||
|
||||
- **WHEN** 代理商点击查看套餐详情
|
||||
- **THEN** 系统显示套餐完整信息
|
||||
- **AND** 包含套餐描述、流量信息、时长等
|
||||
- **AND** 显示定价详情(成本价、建议售价、利润空间、价格来源)
|
||||
- **AND** 显示系列信息
|
||||
|
||||
#### Scenario: 查询未分配的套餐
|
||||
|
||||
- **WHEN** 代理商查询未被分配的套餐ID
|
||||
- **THEN** 系统返回404错误或无权访问错误
|
||||
|
||||
### Requirement: 我的被分配系列列表
|
||||
|
||||
系统 SHALL 提供代理商查询被分配系列的功能。
|
||||
|
||||
#### Scenario: 查询被分配系列列表
|
||||
|
||||
- **WHEN** 代理商访问被分配系列列表
|
||||
- **THEN** 系统显示分配给该代理商的系列列表
|
||||
- **AND** 每个系列包含:分配ID、系列ID、系列编码、系列名称
|
||||
- **AND** 显示定价模式(fixed:固定金额 / percent:百分比)
|
||||
- **AND** 显示定价值(pricing_value)
|
||||
- **AND** 显示分配者店铺名称
|
||||
- **AND** 显示可售套餐数量
|
||||
- **AND** 显示状态
|
||||
- **AND** 支持分页
|
||||
|
||||
### Requirement: 成本价计算规则
|
||||
|
||||
系统 SHALL 根据价格来源计算代理商的成本价。
|
||||
|
||||
#### Scenario: 系列加价模式(series_pricing)
|
||||
|
||||
- **WHEN** 套餐通过系列分配获得定价
|
||||
- **AND** 定价模式为 fixed(固定金额)
|
||||
- **THEN** 成本价 = 套餐价格 + 固定加价金额
|
||||
|
||||
- **WHEN** 定价模式为 percent(百分比)
|
||||
- **THEN** 成本价 = 套餐价格 × (1 + 加价百分比)
|
||||
|
||||
#### Scenario: 单套餐覆盖模式(package_override)
|
||||
|
||||
- **WHEN** 套餐被单独分配并设置了成本价
|
||||
- **THEN** 成本价 = 单套餐分配中设置的成本价
|
||||
- **AND** 价格来源显示为 package_override
|
||||
|
||||
### Requirement: 数据隔离
|
||||
|
||||
系统 SHALL 确保代理商只能查看被分配给自己的套餐。
|
||||
|
||||
#### Scenario: 数据访问隔离
|
||||
|
||||
- **WHEN** 代理商查询可售套餐列表
|
||||
- **THEN** 系统仅返回分配给该代理商的套餐
|
||||
- **AND** 不显示其他代理商的套餐信息
|
||||
|
||||
#### Scenario: 跨代理商访问保护
|
||||
|
||||
- **WHEN** 代理商尝试访问未分配给自己的套餐详情
|
||||
- **THEN** 系统返回403无权访问错误
|
||||
|
||||
### Requirement: 权限控制
|
||||
|
||||
系统 SHALL 对可售套餐查询功能实施权限控制。
|
||||
|
||||
#### Scenario: 仅代理商可访问
|
||||
|
||||
- **WHEN** 非代理商用户访问可售套餐接口
|
||||
- **THEN** 系统返回403无权访问错误
|
||||
|
||||
#### Scenario: 未认证用户访问
|
||||
|
||||
- **WHEN** 未登录用户访问可售套餐接口
|
||||
- **THEN** 系统返回401未认证错误
|
||||
@@ -0,0 +1,159 @@
|
||||
# Package Management Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐列表查询
|
||||
|
||||
系统 SHALL 提供套餐列表查询功能,支持分页和多条件筛选。
|
||||
|
||||
#### Scenario: 查询所有套餐
|
||||
|
||||
- **WHEN** 用户访问套餐管理页面
|
||||
- **THEN** 系统显示套餐列表,包含套餐编码、套餐名称、系列ID、套餐类型、流量、价格、状态、上架状态等
|
||||
- **AND** 支持按套餐名称模糊搜索
|
||||
- **AND** 支持按系列ID筛选
|
||||
- **AND** 支持按状态筛选(启用/禁用)
|
||||
- **AND** 支持按上架状态筛选(上架/下架)
|
||||
- **AND** 支持按套餐类型筛选(formal/addon)
|
||||
- **AND** 支持分页,每页最多100条记录
|
||||
|
||||
#### Scenario: 多条件组合查询
|
||||
|
||||
- **WHEN** 用户同时使用多个筛选条件
|
||||
- **THEN** 系统返回满足所有条件的套餐列表
|
||||
|
||||
### Requirement: 创建套餐
|
||||
|
||||
系统 SHALL 允许管理员创建新的套餐。
|
||||
|
||||
#### Scenario: 成功创建套餐
|
||||
|
||||
- **WHEN** 用户填写必填字段(套餐编码、套餐名称、套餐类型、套餐时长、套餐价格)
|
||||
- **AND** 可选填写系列ID、流量信息、成本价、建议售价等
|
||||
- **AND** 提交表单
|
||||
- **THEN** 系统创建套餐,默认状态为启用,默认上架状态为下架
|
||||
- **AND** 返回创建的套餐详情
|
||||
|
||||
#### Scenario: 套餐编码唯一性验证
|
||||
|
||||
- **WHEN** 用户使用已存在的套餐编码创建套餐
|
||||
- **THEN** 系统返回错误提示"套餐编码已存在"
|
||||
|
||||
#### Scenario: 验证套餐时长范围
|
||||
|
||||
- **WHEN** 套餐时长小于1个月或大于120个月
|
||||
- **THEN** 系统返回验证错误"套餐时长必须在1-120个月之间"
|
||||
|
||||
#### Scenario: 流量类型与流量额度关系
|
||||
|
||||
- **WHEN** 流量类型为真流量(real)
|
||||
- **THEN** 真流量额度(real_data_mb)必须大于0
|
||||
- **AND** 总流量额度(data_amount_mb)等于真流量额度
|
||||
|
||||
- **WHEN** 流量类型为虚流量(virtual)
|
||||
- **THEN** 虚流量额度(virtual_data_mb)必须大于0
|
||||
- **AND** 真流量额度和虚流量额度之和等于总流量额度
|
||||
|
||||
### Requirement: 查看套餐详情
|
||||
|
||||
系统 SHALL 允许用户查看单个套餐的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的套餐
|
||||
|
||||
- **WHEN** 用户通过套餐ID查询详情
|
||||
- **THEN** 系统返回该套餐的完整信息,包括所有字段
|
||||
|
||||
#### Scenario: 查询不存在的套餐
|
||||
|
||||
- **WHEN** 用户查询不存在的套餐ID
|
||||
- **THEN** 系统返回404错误
|
||||
|
||||
### Requirement: 更新套餐
|
||||
|
||||
系统 SHALL 允许管理员更新套餐信息。
|
||||
|
||||
#### Scenario: 成功更新套餐
|
||||
|
||||
- **WHEN** 用户修改套餐的可变字段(名称、时长、价格、流量等)
|
||||
- **AND** 提交更新
|
||||
- **THEN** 系统更新套餐信息
|
||||
- **AND** 返回更新后的套餐详情
|
||||
|
||||
#### Scenario: 套餐编码不可修改
|
||||
|
||||
- **WHEN** 用户尝试修改套餐编码
|
||||
- **THEN** 系统忽略该字段,不允许修改
|
||||
|
||||
### Requirement: 删除套餐
|
||||
|
||||
系统 SHALL 允许管理员删除套餐。
|
||||
|
||||
#### Scenario: 成功删除套餐
|
||||
|
||||
- **WHEN** 用户删除未被使用的套餐
|
||||
- **THEN** 系统删除该套餐
|
||||
- **AND** 返回成功状态
|
||||
|
||||
#### Scenario: 删除被分配的套餐
|
||||
|
||||
- **WHEN** 用户删除已被分配给代理商的套餐
|
||||
- **THEN** 系统返回错误提示"该套餐已被分配,无法删除"
|
||||
|
||||
### Requirement: 套餐状态管理
|
||||
|
||||
系统 SHALL 支持套餐启用/禁用状态管理。
|
||||
|
||||
#### Scenario: 启用套餐
|
||||
|
||||
- **WHEN** 用户将禁用状态的套餐切换为启用
|
||||
- **THEN** 系统更新状态为启用(status=1)
|
||||
|
||||
#### Scenario: 禁用套餐
|
||||
|
||||
- **WHEN** 用户将启用状态的套餐切换为禁用
|
||||
- **THEN** 系统更新状态为禁用(status=2)
|
||||
- **AND** 该套餐将不可用于新的分配或充值
|
||||
|
||||
### Requirement: 套餐上架状态管理
|
||||
|
||||
系统 SHALL 支持套餐上架/下架状态管理。
|
||||
|
||||
#### Scenario: 上架套餐
|
||||
|
||||
- **WHEN** 用户将下架状态的套餐切换为上架
|
||||
- **THEN** 系统更新上架状态为上架(shelf_status=1)
|
||||
- **AND** 该套餐将对代理商可见
|
||||
|
||||
#### Scenario: 下架套餐
|
||||
|
||||
- **WHEN** 用户将上架状态的套餐切换为下架
|
||||
- **THEN** 系统更新上架状态为下架(shelf_status=2)
|
||||
- **AND** 该套餐将对代理商不可见
|
||||
|
||||
### Requirement: 套餐类型支持
|
||||
|
||||
系统 SHALL 支持两种套餐类型。
|
||||
|
||||
#### Scenario: 正式套餐
|
||||
|
||||
- **WHEN** 创建套餐类型为 formal 的套餐
|
||||
- **THEN** 系统记录为正式套餐
|
||||
|
||||
#### Scenario: 附加套餐
|
||||
|
||||
- **WHEN** 创建套餐类型为 addon 的套餐
|
||||
- **THEN** 系统记录为附加套餐
|
||||
|
||||
### Requirement: 权限控制
|
||||
|
||||
系统 SHALL 对套餐管理功能实施权限控制。
|
||||
|
||||
#### Scenario: 未认证用户访问
|
||||
|
||||
- **WHEN** 未登录用户访问套餐管理接口
|
||||
- **THEN** 系统返回401未认证错误
|
||||
|
||||
#### Scenario: 无权限用户访问
|
||||
|
||||
- **WHEN** 已登录但无权限的用户访问套餐管理接口
|
||||
- **THEN** 系统返回403无权访问错误
|
||||
@@ -0,0 +1,116 @@
|
||||
# Package Series Management Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐系列列表查询
|
||||
|
||||
系统 SHALL 提供套餐系列列表查询功能,支持分页和条件筛选。
|
||||
|
||||
#### Scenario: 查询所有套餐系列
|
||||
|
||||
- **WHEN** 用户访问套餐系列管理页面
|
||||
- **THEN** 系统显示套餐系列列表,包含系列名称、系列编码、描述、状态、创建时间、更新时间
|
||||
- **AND** 支持按系列名称模糊搜索
|
||||
- **AND** 支持按状态筛选(启用/禁用)
|
||||
- **AND** 支持分页,每页最多100条记录
|
||||
|
||||
#### Scenario: 空列表处理
|
||||
|
||||
- **WHEN** 没有符合条件的套餐系列
|
||||
- **THEN** 系统显示空状态提示
|
||||
|
||||
### Requirement: 创建套餐系列
|
||||
|
||||
系统 SHALL 允许管理员创建新的套餐系列。
|
||||
|
||||
#### Scenario: 成功创建套餐系列
|
||||
|
||||
- **WHEN** 用户填写系列编码、系列名称(必填)
|
||||
- **AND** 可选填写描述(最大500字符)
|
||||
- **AND** 提交表单
|
||||
- **THEN** 系统创建套餐系列,默认状态为启用
|
||||
- **AND** 返回创建的套餐系列详情
|
||||
|
||||
#### Scenario: 系列编码重复
|
||||
|
||||
- **WHEN** 用户使用已存在的系列编码创建套餐系列
|
||||
- **THEN** 系统返回错误提示"系列编码已存在"
|
||||
|
||||
#### Scenario: 验证系列名称长度
|
||||
|
||||
- **WHEN** 系列名称长度小于1或大于255个字符
|
||||
- **THEN** 系统返回验证错误
|
||||
|
||||
### Requirement: 查看套餐系列详情
|
||||
|
||||
系统 SHALL 允许用户查看单个套餐系列的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的套餐系列
|
||||
|
||||
- **WHEN** 用户通过系列ID查询详情
|
||||
- **THEN** 系统返回该套餐系列的完整信息
|
||||
|
||||
#### Scenario: 查询不存在的套餐系列
|
||||
|
||||
- **WHEN** 用户查询不存在的系列ID
|
||||
- **THEN** 系统返回404错误
|
||||
|
||||
### Requirement: 更新套餐系列
|
||||
|
||||
系统 SHALL 允许管理员更新套餐系列信息。
|
||||
|
||||
#### Scenario: 成功更新套餐系列
|
||||
|
||||
- **WHEN** 用户修改系列名称或描述
|
||||
- **AND** 提交更新
|
||||
- **THEN** 系统更新套餐系列信息
|
||||
- **AND** 返回更新后的套餐系列详情
|
||||
|
||||
#### Scenario: 系列编码不可修改
|
||||
|
||||
- **WHEN** 用户尝试修改系列编码
|
||||
- **THEN** 系统忽略该字段,不允许修改
|
||||
|
||||
### Requirement: 删除套餐系列
|
||||
|
||||
系统 SHALL 允许管理员删除套餐系列。
|
||||
|
||||
#### Scenario: 成功删除套餐系列
|
||||
|
||||
- **WHEN** 用户删除未被套餐使用的系列
|
||||
- **THEN** 系统删除该套餐系列
|
||||
- **AND** 返回成功状态
|
||||
|
||||
#### Scenario: 删除被使用的套餐系列
|
||||
|
||||
- **WHEN** 用户删除已被套餐关联的系列
|
||||
- **THEN** 系统返回错误提示"该系列下存在套餐,无法删除"
|
||||
|
||||
### Requirement: 套餐系列状态管理
|
||||
|
||||
系统 SHALL 支持套餐系列状态的开关管理。
|
||||
|
||||
#### Scenario: 启用套餐系列
|
||||
|
||||
- **WHEN** 用户将禁用状态的系列切换为启用
|
||||
- **THEN** 系统更新状态为启用(status=1)
|
||||
|
||||
#### Scenario: 禁用套餐系列
|
||||
|
||||
- **WHEN** 用户将启用状态的系列切换为禁用
|
||||
- **THEN** 系统更新状态为禁用(status=2)
|
||||
- **AND** 该系列下的套餐可能受到影响(业务规则)
|
||||
|
||||
### Requirement: 权限控制
|
||||
|
||||
系统 SHALL 对套餐系列管理功能实施权限控制。
|
||||
|
||||
#### Scenario: 未认证用户访问
|
||||
|
||||
- **WHEN** 未登录用户访问套餐系列管理接口
|
||||
- **THEN** 系统返回401未认证错误
|
||||
|
||||
#### Scenario: 无权限用户访问
|
||||
|
||||
- **WHEN** 已登录但无权限的用户访问套餐系列管理接口
|
||||
- **THEN** 系统返回403无权访问错误
|
||||
@@ -0,0 +1,164 @@
|
||||
# Shop Package Allocation (单套餐分配) Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 单套餐分配列表查询
|
||||
|
||||
系统 SHALL 提供单套餐分配记录的查询功能。
|
||||
|
||||
#### Scenario: 查询所有单套餐分配
|
||||
|
||||
- **WHEN** 管理员访问单套餐分配管理页面
|
||||
- **THEN** 系统显示单套餐分配列表
|
||||
- **AND** 每条记录包含:分配ID、套餐ID、套餐编码、套餐名称
|
||||
- **AND** 显示被分配的店铺ID和店铺名称
|
||||
- **AND** 显示覆盖的成本价(cost_price,单位:分)
|
||||
- **AND** 显示原计算成本价(calculated_cost_price,供参考)
|
||||
- **AND** 显示关联的系列分配ID
|
||||
- **AND** 显示状态、创建时间、更新时间
|
||||
- **AND** 支持分页,每页最多100条记录
|
||||
|
||||
#### Scenario: 按店铺筛选
|
||||
|
||||
- **WHEN** 管理员选择特定店铺ID筛选
|
||||
- **THEN** 系统返回该店铺的所有单套餐分配记录
|
||||
|
||||
#### Scenario: 按套餐筛选
|
||||
|
||||
- **WHEN** 管理员选择特定套餐ID筛选
|
||||
- **THEN** 系统返回该套餐的所有分配记录
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
|
||||
- **WHEN** 管理员选择状态(启用/禁用)筛选
|
||||
- **THEN** 系统返回符合状态条件的分配记录
|
||||
|
||||
### Requirement: 创建单套餐分配
|
||||
|
||||
系统 SHALL 允许管理员为店铺分配特定套餐并设置成本价。
|
||||
|
||||
#### Scenario: 成功创建单套餐分配
|
||||
|
||||
- **WHEN** 管理员选择套餐ID、店铺ID
|
||||
- **AND** 设置覆盖的成本价(必填,最小值为0)
|
||||
- **AND** 提交分配
|
||||
- **THEN** 系统创建单套餐分配记录
|
||||
- **AND** 计算并保存原计算成本价(基于系列分配规则)
|
||||
- **AND** 返回创建的分配详情,包括关联的系列分配ID
|
||||
|
||||
#### Scenario: 重复分配检查
|
||||
|
||||
- **WHEN** 管理员为同一店铺分配已分配过的套餐
|
||||
- **THEN** 系统返回错误提示"该套餐已分配给此店铺"
|
||||
|
||||
#### Scenario: 店铺和套餐验证
|
||||
|
||||
- **WHEN** 管理员使用不存在的店铺ID或套餐ID
|
||||
- **THEN** 系统返回错误提示"店铺或套餐不存在"
|
||||
|
||||
### Requirement: 查看单套餐分配详情
|
||||
|
||||
系统 SHALL 允许管理员查看单个分配记录的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的分配记录
|
||||
|
||||
- **WHEN** 管理员通过分配ID查询详情
|
||||
- **THEN** 系统返回该分配记录的完整信息
|
||||
- **AND** 包含套餐完整信息、店铺信息、定价信息
|
||||
|
||||
#### Scenario: 查询不存在的分配记录
|
||||
|
||||
- **WHEN** 管理员查询不存在的分配ID
|
||||
- **THEN** 系统返回404错误
|
||||
|
||||
### Requirement: 更新单套餐分配
|
||||
|
||||
系统 SHALL 允许管理员更新单套餐分配的成本价。
|
||||
|
||||
#### Scenario: 成功更新成本价
|
||||
|
||||
- **WHEN** 管理员修改覆盖的成本价
|
||||
- **AND** 提交更新
|
||||
- **THEN** 系统更新成本价
|
||||
- **AND** 返回更新后的分配详情
|
||||
|
||||
#### Scenario: 成本价验证
|
||||
|
||||
- **WHEN** 管理员设置成本价小于0
|
||||
- **THEN** 系统返回验证错误"成本价必须大于等于0"
|
||||
|
||||
### Requirement: 删除单套餐分配
|
||||
|
||||
系统 SHALL 允许管理员删除单套餐分配记录。
|
||||
|
||||
#### Scenario: 成功删除分配
|
||||
|
||||
- **WHEN** 管理员删除单套餐分配记录
|
||||
- **THEN** 系统删除该分配记录
|
||||
- **AND** 该店铺将无法再以此定价售卖该套餐
|
||||
- **AND** 返回成功状态
|
||||
|
||||
#### Scenario: 删除正在使用的分配
|
||||
|
||||
- **WHEN** 管理员删除正在被订单使用的分配记录
|
||||
- **THEN** 系统可能返回警告或阻止删除(根据业务规则)
|
||||
|
||||
### Requirement: 单套餐分配状态管理
|
||||
|
||||
系统 SHALL 支持单套餐分配的启用/禁用状态管理。
|
||||
|
||||
#### Scenario: 启用分配
|
||||
|
||||
- **WHEN** 管理员将禁用状态的分配切换为启用
|
||||
- **THEN** 系统更新状态为启用(status=1)
|
||||
- **AND** 该店铺可以使用此定价售卖套餐
|
||||
|
||||
#### Scenario: 禁用分配
|
||||
|
||||
- **WHEN** 管理员将启用状态的分配切换为禁用
|
||||
- **THEN** 系统更新状态为禁用(status=2)
|
||||
- **AND** 该店铺将无法使用此定价售卖套餐
|
||||
|
||||
### Requirement: 成本价优先级规则
|
||||
|
||||
系统 SHALL 实现单套餐分配成本价覆盖系列分配规则。
|
||||
|
||||
#### Scenario: 单套餐分配优先
|
||||
|
||||
- **WHEN** 店铺同时拥有系列分配和单套餐分配
|
||||
- **THEN** 系统使用单套餐分配的成本价
|
||||
- **AND** 原计算成本价(calculated_cost_price)保存系列分配规则计算的价格,供参考
|
||||
|
||||
#### Scenario: 仅系列分配
|
||||
|
||||
- **WHEN** 店铺只有系列分配,没有单套餐分配
|
||||
- **THEN** 系统使用系列分配规则计算成本价
|
||||
|
||||
### Requirement: 关联系列分配追踪
|
||||
|
||||
系统 SHALL 追踪单套餐分配与系列分配的关联关系。
|
||||
|
||||
#### Scenario: 记录关联的系列分配
|
||||
|
||||
- **WHEN** 创建单套餐分配时
|
||||
- **THEN** 系统记录关联的系列分配ID(allocation_id)
|
||||
- **AND** 用于追溯定价来源
|
||||
|
||||
### Requirement: 权限控制
|
||||
|
||||
系统 SHALL 对单套餐分配管理功能实施权限控制。
|
||||
|
||||
#### Scenario: 未认证用户访问
|
||||
|
||||
- **WHEN** 未登录用户访问单套餐分配接口
|
||||
- **THEN** 系统返回401未认证错误
|
||||
|
||||
#### Scenario: 无权限用户访问
|
||||
|
||||
- **WHEN** 已登录但无权限的用户访问单套餐分配接口
|
||||
- **THEN** 系统返回403无权访问错误
|
||||
|
||||
#### Scenario: 仅管理员可操作
|
||||
|
||||
- **WHEN** 非管理员用户尝试创建、更新或删除单套餐分配
|
||||
- **THEN** 系统返回403无权访问错误
|
||||
156
openspec/changes/add-package-management-system/tasks.md
Normal file
156
openspec/changes/add-package-management-system/tasks.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 基础设施准备
|
||||
|
||||
- [ ] 1.1 创建套餐管理类型定义文件(src/types/api/packageManagement.ts)
|
||||
- [ ] 1.2 创建套餐常量配置文件(src/config/constants/package.ts)
|
||||
- [ ] 1.3 导出常量配置到 constants/index.ts
|
||||
|
||||
## 2. API 服务层实现
|
||||
|
||||
### 2.1 套餐系列 API(packageSeries.ts)
|
||||
- [ ] 2.1.1 实现 getPackageSeries(套餐系列列表)
|
||||
- [ ] 2.1.2 实现 createPackageSeries(创建套餐系列)
|
||||
- [ ] 2.1.3 实现 getPackageSeriesDetail(获取套餐系列详情)
|
||||
- [ ] 2.1.4 实现 updatePackageSeries(更新套餐系列)
|
||||
- [ ] 2.1.5 实现 deletePackageSeries(删除套餐系列)
|
||||
- [ ] 2.1.6 实现 updatePackageSeriesStatus(更新套餐系列状态)
|
||||
|
||||
### 2.2 套餐管理 API(package.ts)
|
||||
- [ ] 2.2.1 实现 getPackages(套餐列表)
|
||||
- [ ] 2.2.2 实现 createPackage(创建套餐)
|
||||
- [ ] 2.2.3 实现 getPackageDetail(获取套餐详情)
|
||||
- [ ] 2.2.4 实现 updatePackage(更新套餐)
|
||||
- [ ] 2.2.5 实现 deletePackage(删除套餐)
|
||||
- [ ] 2.2.6 实现 updatePackageStatus(更新套餐状态)
|
||||
- [ ] 2.2.7 实现 updatePackageShelfStatus(更新套餐上架状态)
|
||||
|
||||
### 2.3 代理可售套餐 API(myPackage.ts)
|
||||
- [ ] 2.3.1 实现 getMyPackages(我的可售套餐列表)
|
||||
- [ ] 2.3.2 实现 getMyPackageDetail(获取可售套餐详情)
|
||||
- [ ] 2.3.3 实现 getMySeriesAllocations(我的被分配系列列表)
|
||||
|
||||
### 2.4 单套餐分配 API(shopPackageAllocation.ts)
|
||||
- [ ] 2.4.1 实现 getShopPackageAllocations(单套餐分配列表)
|
||||
- [ ] 2.4.2 实现 createShopPackageAllocation(创建单套餐分配)
|
||||
- [ ] 2.4.3 实现 getShopPackageAllocationDetail(获取单套餐分配详情)
|
||||
- [ ] 2.4.4 实现 updateShopPackageAllocation(更新单套餐分配)
|
||||
- [ ] 2.4.5 实现 deleteShopPackageAllocation(删除单套餐分配)
|
||||
- [ ] 2.4.6 实现 updateShopPackageAllocationStatus(更新单套餐分配状态)
|
||||
|
||||
- [ ] 2.5 在 src/api/modules/index.ts 中导出所有新服务
|
||||
|
||||
## 3. 页面实现
|
||||
|
||||
### 3.1 套餐系列管理页面(package-series/index.vue)
|
||||
- [ ] 3.1.1 实现列表展示(表格、分页)
|
||||
- [ ] 3.1.2 实现搜索栏(系列名称、状态筛选)
|
||||
- [ ] 3.1.3 实现新增对话框(表单验证)
|
||||
- [ ] 3.1.4 实现编辑功能(复用新增对话框,根据 dialogType 区分新增/编辑)
|
||||
- [ ] 3.1.5 实现删除功能(二次确认)
|
||||
- [ ] 3.1.6 实现状态开关(启用/禁用)
|
||||
- [ ] 3.1.7 集成 API 服务并处理加载状态
|
||||
|
||||
### 3.2 套餐管理页面(package-list/index.vue)
|
||||
- [ ] 3.2.1 实现列表展示(表格、分页)
|
||||
- [ ] 3.2.2 实现搜索栏(名称、系列、状态、上架状态、类型筛选)
|
||||
- [ ] 3.2.3 实现系列下拉选择器(加载套餐系列列表,只显示启用状态)
|
||||
- [ ] 3.2.4 实现新增对话框(表单验证、系列选择)
|
||||
- [ ] 3.2.5 实现编辑功能(复用新增对话框,根据 dialogType 区分新增/编辑)
|
||||
- [ ] 3.2.6 实现删除功能(二次确认)
|
||||
- [ ] 3.2.7 实现状态开关(启用/禁用)
|
||||
- [ ] 3.2.8 实现上架状态开关(上架/下架)
|
||||
- [ ] 3.2.9 集成 API 服务并处理加载状态
|
||||
|
||||
### 3.3 代理可售套餐页面(my-packages/index.vue)
|
||||
- [ ] 3.3.1 创建页面文件和基本结构
|
||||
- [ ] 3.3.2 实现列表展示(表格、分页)
|
||||
- [ ] 3.3.3 实现搜索栏(系列、类型筛选)
|
||||
- [ ] 3.3.4 实现详情对话框(显示成本价、建议售价、利润空间)
|
||||
- [ ] 3.3.5 实现被分配系列列表Tab(可选)
|
||||
- [ ] 3.3.6 集成 API 服务并处理加载状态
|
||||
|
||||
### 3.4 单套餐分配页面(package-assign/index.vue)
|
||||
- [ ] 3.4.1 创建页面文件和基本结构
|
||||
- [ ] 3.4.2 实现列表展示(表格、分页)
|
||||
- [ ] 3.4.3 实现搜索栏(店铺、套餐、状态筛选)
|
||||
- [ ] 3.4.4 实现套餐下拉选择器(加载套餐列表,只显示启用且上架的套餐)
|
||||
- [ ] 3.4.5 实现店铺下拉选择器(使用 ShopService 加载店铺列表)
|
||||
- [ ] 3.4.6 实现新增对话框(套餐选择、店铺选择、成本价输入)
|
||||
- [ ] 3.4.7 实现编辑功能(单独对话框或复用新增对话框,只允许修改成本价)
|
||||
- [ ] 3.4.8 实现删除功能(二次确认)
|
||||
- [ ] 3.4.9 实现状态管理(启用/禁用开关)
|
||||
- [ ] 3.4.10 集成 API 服务并处理加载状态
|
||||
|
||||
## 4. 路由配置
|
||||
|
||||
- [ ] 4.1 在 asyncRoutes.ts 中添加 my-packages 路由配置
|
||||
- [ ] 4.2 验证路由权限配置正确
|
||||
|
||||
## 5. 集成测试
|
||||
|
||||
### 5.1 套餐系列管理测试
|
||||
- [ ] 5.1.1 测试列表查询(空列表、有数据、分页)
|
||||
- [ ] 5.1.2 测试搜索功能(名称模糊搜索、状态筛选)
|
||||
- [ ] 5.1.3 测试新增功能(成功、编码重复、字段验证)
|
||||
- [ ] 5.1.4 测试编辑功能(成功、字段验证)
|
||||
- [ ] 5.1.5 测试删除功能(成功、有关联套餐时禁止删除)
|
||||
- [ ] 5.1.6 测试状态切换(启用→禁用、禁用→启用)
|
||||
- [ ] 5.1.7 测试权限控制(未登录、无权限)
|
||||
|
||||
### 5.2 套餐管理测试
|
||||
- [ ] 5.2.1 测试列表查询(空列表、有数据、分页)
|
||||
- [ ] 5.2.2 测试多条件筛选(名称、系列、状态、上架状态、类型)
|
||||
- [ ] 5.2.3 测试系列下拉选择器(只显示启用状态的系列)
|
||||
- [ ] 5.2.4 测试新增功能(成功、编码重复、时长验证、流量验证)
|
||||
- [ ] 5.2.5 测试编辑功能(成功、字段验证)
|
||||
- [ ] 5.2.6 测试删除功能(成功、已分配时禁止删除)
|
||||
- [ ] 5.2.7 测试状态切换(启用→禁用、禁用→启用)
|
||||
- [ ] 5.2.8 测试上架状态切换(上架→下架、下架→上架)
|
||||
- [ ] 5.2.9 测试权限控制(未登录、无权限)
|
||||
|
||||
### 5.3 代理可售套餐测试
|
||||
- [ ] 5.3.1 测试列表查询(空列表、有数据、分页)
|
||||
- [ ] 5.3.2 测试筛选功能(按系列、按类型)
|
||||
- [ ] 5.3.3 测试详情查询(显示成本价、建议售价、利润空间、价格来源)
|
||||
- [ ] 5.3.4 测试数据隔离(只能看到分配给自己的套餐)
|
||||
- [ ] 5.3.5 测试被分配系列列表(如果实现)
|
||||
- [ ] 5.3.6 测试权限控制(非代理商用户无法访问)
|
||||
|
||||
### 5.4 单套餐分配测试
|
||||
- [ ] 5.4.1 测试列表查询(空列表、有数据、分页)
|
||||
- [ ] 5.4.2 测试筛选功能(按店铺、按套餐、按状态)
|
||||
- [ ] 5.4.3 测试套餐下拉选择器(只显示启用且上架的套餐)
|
||||
- [ ] 5.4.4 测试店铺下拉选择器(加载店铺列表)
|
||||
- [ ] 5.4.5 测试新增功能(成功、重复分配、成本价验证)
|
||||
- [ ] 5.4.6 测试编辑功能(修改成本价)
|
||||
- [ ] 5.4.7 测试删除功能(成功、有订单时的处理)
|
||||
- [ ] 5.4.8 测试状态切换(启用→禁用、禁用→启用)
|
||||
- [ ] 5.4.9 测试价格覆盖规则(单套餐分配优先于系列分配)
|
||||
- [ ] 5.4.10 测试权限控制(仅管理员可操作)
|
||||
|
||||
### 5.5 通用功能测试
|
||||
- [ ] 5.5.1 测试所有页面的表单验证(必填、长度、格式)
|
||||
- [ ] 5.5.2 测试所有页面的 loading 状态(列表、提交、删除)
|
||||
- [ ] 5.5.3 测试所有页面的错误处理(网络错误、业务错误)
|
||||
- [ ] 5.5.4 测试所有页面的二次确认(删除操作)
|
||||
- [ ] 5.5.5 测试分页功能(换页、改变每页数量)
|
||||
- [ ] 5.5.6 测试刷新功能(列表刷新)
|
||||
- [ ] 5.5.7 测试列显示/隐藏功能
|
||||
- [ ] 5.5.8 测试状态值映射(前端0/1与后端1/2的转换)
|
||||
|
||||
## 6. 代码优化和文档
|
||||
|
||||
- [ ] 6.1 代码格式化和 ESLint 检查
|
||||
- [ ] 6.2 添加必要的注释
|
||||
- [ ] 6.3 更新 API 文档(如需要)
|
||||
- [ ] 6.4 提交代码并创建 PR
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有页面需参考 `/system/role` 页面的组件化结构
|
||||
- 使用统一的 `CommonStatus` 常量(需要注意文档中的状态值映射)
|
||||
- API 字段使用下划线命名(如 `series_name`),前端类型使用驼峰命名
|
||||
- 所有删除操作需要二次确认
|
||||
- 所有表单需要完整的验证规则
|
||||
- 统一使用 Element Plus 的 Message 和 MessageBox 组件
|
||||
190
openspec/changes/update-series-allocation-commission/design.md
Normal file
190
openspec/changes/update-series-allocation-commission/design.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Design: 套餐系列分配佣金系统重构
|
||||
|
||||
## Context
|
||||
|
||||
当前系统使用简单的定价模式(pricing_mode/pricing_value)和一次性佣金(one_time_commission_*)来管理套餐系列分配。随着业务发展,需要更复杂的佣金系统:
|
||||
- 支持基础返佣(固定金额或百分比)
|
||||
- 支持梯度返佣(根据销量或销售额分档返佣)
|
||||
- 更清晰的数据模型和API接口
|
||||
|
||||
**背景约束**:
|
||||
- 前后端需要同步部署(Breaking Change)
|
||||
- 需要数据迁移方案
|
||||
- 影响现有的套餐系列分配功能
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals**:
|
||||
- 实现新的佣金配置模型,支持基础返佣和梯度返佣
|
||||
- 提供清晰的UI界面让用户配置复杂的返佣规则
|
||||
- 保证数据一致性和类型安全
|
||||
- 提供良好的用户体验(表单验证、错误提示)
|
||||
|
||||
**Non-Goals**:
|
||||
- 不处理历史数据的完整性验证(由后端负责)
|
||||
- 不实现佣金计算逻辑(由后端负责)
|
||||
- 不处理佣金结算流程(属于其他模块)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Data Model Design
|
||||
|
||||
**决策**: 采用嵌套对象结构表示佣金配置
|
||||
|
||||
**理由**:
|
||||
- `base_commission: { mode, value }` - 清晰表达基础返佣的两个维度
|
||||
- `tier_config: { period_type, tier_type, tiers[] }` - 梯度配置与基础配置分离,可选性强
|
||||
- `tiers: [{ threshold, mode, value }]` - 每个档位都有独立的返佣模式和值
|
||||
|
||||
**替代方案考虑**:
|
||||
- ❌ 平铺所有字段 - 会导致字段过多,语义不清晰
|
||||
- ❌ 使用JSON字符串存储配置 - 失去类型安全,不利于表单编辑
|
||||
|
||||
### 2. UI Design Pattern
|
||||
|
||||
**决策**: 使用渐进式表单设计
|
||||
|
||||
**表单结构**:
|
||||
```
|
||||
1. 基础返佣配置 (必填)
|
||||
- 返佣模式: 单选 (固定金额/百分比)
|
||||
- 返佣值: 数字输入
|
||||
|
||||
2. 梯度返佣设置 (可选)
|
||||
- 启用开关: 是/否
|
||||
- [如果启用]:
|
||||
- 周期类型: 下拉 (月度/季度/年度)
|
||||
- 梯度类型: 下拉 (销量/销售额)
|
||||
- 档位列表: 动态表单
|
||||
* 阈值
|
||||
* 返佣模式
|
||||
* 返佣值
|
||||
* [添加]/[删除] 按钮
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 渐进式设计降低初始复杂度
|
||||
- 只有启用梯度返佣时才显示相关配置
|
||||
- 动态档位列表提供灵活性
|
||||
|
||||
**替代方案考虑**:
|
||||
- ❌ 全部平铺展示 - 对不需要梯度返佣的用户造成困扰
|
||||
- ❌ 使用向导模式 - 增加操作步骤,不适合编辑场景
|
||||
|
||||
### 3. Form Validation Strategy
|
||||
|
||||
**决策**: 分层验证 + 条件验证
|
||||
|
||||
**验证规则**:
|
||||
1. 基础返佣配置:
|
||||
- mode: 必选
|
||||
- value: 必填,>= 0
|
||||
|
||||
2. 梯度返佣配置(当启用时):
|
||||
- period_type: 必选
|
||||
- tier_type: 必选
|
||||
- tiers: 至少一个档位
|
||||
- 每个档位:
|
||||
- threshold: 必填,> 0
|
||||
- mode: 必选
|
||||
- value: 必填,> 0
|
||||
- 档位阈值必须递增
|
||||
|
||||
**实现方式**:
|
||||
- 使用 Element Plus 的表单验证
|
||||
- 自定义validator处理档位阈值递增验证
|
||||
- 使用 computed 动态生成验证规则
|
||||
|
||||
### 4. API Response Handling
|
||||
|
||||
**决策**: 统一使用 `list` 字段名,添加适配层
|
||||
|
||||
**理由**:
|
||||
- 后端统一规范使用 `list` 而非 `items`
|
||||
- 添加 `total_pages` 字段提供更完整的分页信息
|
||||
- 保持前端代码与后端规范一致
|
||||
|
||||
**迁移策略**:
|
||||
```typescript
|
||||
// Before
|
||||
const res = await getShopSeriesAllocations(params)
|
||||
allocationList.value = res.data.items || []
|
||||
|
||||
// After
|
||||
const res = await getShopSeriesAllocations(params)
|
||||
allocationList.value = res.data.list || []
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: Breaking Change导致部署协调困难
|
||||
|
||||
**风险**: 前后端必须同时部署,否则会出现接口不兼容
|
||||
|
||||
**缓解措施**:
|
||||
- 与后端团队协调部署窗口
|
||||
- 准备回退方案(前端代码分支)
|
||||
- 在测试环境充分验证
|
||||
|
||||
### Risk 2: 复杂表单导致用户体验问题
|
||||
|
||||
**风险**: 梯度返佣配置较复杂,用户可能不理解
|
||||
|
||||
**缓解措施**:
|
||||
- 提供清晰的字段说明
|
||||
- 添加示例或帮助文档链接
|
||||
- 使用默认值简化初次配置
|
||||
|
||||
### Risk 3: 数据迁移可能失败
|
||||
|
||||
**风险**: 旧数据无法完全转换为新模型
|
||||
|
||||
**缓解措施**:
|
||||
- 要求后端提供数据迁移脚本和验证
|
||||
- 在迁移后检查数据完整性
|
||||
- 保留旧数据备份
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: 准备阶段
|
||||
1. 与后端确认API变更细节和时间表
|
||||
2. 在开发环境实现前端变更
|
||||
3. 与后端在测试环境联调
|
||||
|
||||
### Phase 2: 测试阶段
|
||||
1. 功能测试: 创建、编辑、删除、列表展示
|
||||
2. 集成测试: 与后端API集成
|
||||
3. 用户验收测试: 业务人员验证
|
||||
|
||||
### Phase 3: 部署阶段
|
||||
1. 准备回退方案
|
||||
2. 与后端协调部署窗口
|
||||
3. 同步部署前后端
|
||||
4. 验证生产环境功能
|
||||
|
||||
### Phase 4: 监控阶段
|
||||
1. 监控API错误率
|
||||
2. 收集用户反馈
|
||||
3. 修复遗留问题
|
||||
|
||||
### Rollback Plan
|
||||
|
||||
如果部署后发现严重问题:
|
||||
1. 前端回退到上一版本
|
||||
2. 后端回退API(如果可能)
|
||||
3. 通知用户暂时不可用
|
||||
4. 修复问题后重新部署
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Q**: 梯度返佣的档位数量是否有上限?
|
||||
- **A**: 待后端确认,前端可以先不限制或设置合理上限(如10个)
|
||||
|
||||
2. **Q**: 返佣值的单位和精度如何处理?
|
||||
- **A**: 固定金额使用"分"为单位,百分比使用千分比(如200=20%),待后端确认
|
||||
|
||||
3. **Q**: 是否需要在列表页显示梯度返佣信息?
|
||||
- **A**: 暂时只显示基础返佣,梯度信息在详情或编辑时查看
|
||||
|
||||
4. **Q**: 旧数据如何映射到新模型?
|
||||
- **A**: 待后端提供迁移方案,前端需要能够正确显示迁移后的数据
|
||||
@@ -0,0 +1,46 @@
|
||||
# Change: 更新套餐系列分配佣金系统
|
||||
|
||||
## Why
|
||||
|
||||
当前套餐系列分配使用简单的定价模式(固定加价/百分比加价)和一次性佣金配置,不支持复杂的返佣规则。新的业务需求要求支持:
|
||||
- 基础返佣配置(固定金额或百分比)
|
||||
- 梯度返佣系统(根据销量或销售额分档返佣)
|
||||
- 更灵活的佣金计算模型
|
||||
|
||||
## What Changes
|
||||
|
||||
**BREAKING** - 完全重构套餐系列分配的数据模型和API接口:
|
||||
|
||||
- 移除旧的定价字段: `pricing_mode`, `pricing_value`, `calculated_cost_price`
|
||||
- 移除旧的一次性佣金字段: `one_time_commission_trigger`, `one_time_commission_threshold`, `one_time_commission_amount`
|
||||
- 新增基础返佣配置: `base_commission` (包含 mode 和 value)
|
||||
- 新增梯度返佣开关: `enable_tier_commission`
|
||||
- 新增梯度返佣配置: `tier_config` (可选,当启用梯度返佣时需要)
|
||||
- 响应数据结构从 `items` 改为 `list` (符合后端统一规范)
|
||||
- 更新所有相关的创建/更新接口以支持新的佣金模型
|
||||
|
||||
## Impact
|
||||
|
||||
- **影响模块**: 套餐系列分配 (`/package-management/series-assign`)
|
||||
- **受影响文件**:
|
||||
- `src/types/api/packageManagement.ts` - TypeScript类型定义
|
||||
- `src/api/modules/shopSeriesAllocation.ts` - API服务层
|
||||
- `src/views/package-management/series-assign/index.vue` - 前端页面
|
||||
- **数据迁移**: 需要后端提供数据迁移方案,将旧的定价数据转换为新的佣金配置
|
||||
- **向后兼容性**: **不兼容**,需要前后端同时部署
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
1. **API响应结构变更**:
|
||||
- 列表接口响应从 `{ items, page, page_size, total }` 改为 `{ list, page, page_size, total, total_pages }`
|
||||
- 移除 `pricing_mode`, `pricing_value`, `calculated_cost_price` 字段
|
||||
- 移除一次性佣金相关字段
|
||||
- 新增 `base_commission`, `enable_tier_commission` 字段
|
||||
|
||||
2. **API请求结构变更**:
|
||||
- 创建/更新接口需要新的 `base_commission` 对象
|
||||
- 支持可选的 `enable_tier_commission` 和 `tier_config`
|
||||
|
||||
3. **前端组件变更**:
|
||||
- 表单需要支持基础佣金配置和梯度返佣配置
|
||||
- 表格列需要显示新的佣金信息
|
||||
@@ -0,0 +1,225 @@
|
||||
# Package Series Allocation Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 套餐系列分配列表查询
|
||||
|
||||
系统 SHALL 提供套餐系列分配列表查询功能,支持按店铺、系列和状态筛选,返回包含新佣金配置的分配信息。
|
||||
|
||||
#### Scenario: 成功获取分配列表
|
||||
|
||||
- **WHEN** 用户请求套餐系列分配列表
|
||||
- **THEN** 系统返回分配列表,每项包含:
|
||||
- 基本信息: id, series_id, series_name, shop_id, shop_name
|
||||
- 分配者信息: allocator_shop_id, allocator_shop_name
|
||||
- 佣金配置: base_commission (mode, value), enable_tier_commission
|
||||
- 状态和时间: status, created_at, updated_at
|
||||
- **AND** 响应结构为: `{ list, page, page_size, total, total_pages }`
|
||||
|
||||
#### Scenario: 按条件筛选分配列表
|
||||
|
||||
- **WHEN** 用户提供筛选条件 (shop_id, series_id, status)
|
||||
- **THEN** 系统返回符合条件的分配列表
|
||||
- **AND** 支持分页参数 (page, page_size)
|
||||
|
||||
### Requirement: 创建套餐系列分配
|
||||
|
||||
系统 SHALL 允许用户创建套餐系列分配,配置基础返佣和可选的梯度返佣规则。
|
||||
|
||||
#### Scenario: 创建基础返佣分配
|
||||
|
||||
- **WHEN** 用户提交创建请求,包含:
|
||||
- series_id: 套餐系列ID
|
||||
- shop_id: 被分配的店铺ID
|
||||
- base_commission: { mode: "fixed"/"percent", value: number }
|
||||
- **THEN** 系统创建分配记录
|
||||
- **AND** 返回完整的分配信息包含 allocator_shop_id 和 allocator_shop_name
|
||||
|
||||
#### Scenario: 创建带梯度返佣的分配
|
||||
|
||||
- **WHEN** 用户提交创建请求,包含:
|
||||
- series_id, shop_id, base_commission (同上)
|
||||
- enable_tier_commission: true
|
||||
- tier_config: { period_type, tier_type, tiers[] }
|
||||
- period_type: "monthly"/"quarterly"/"yearly"
|
||||
- tier_type: "sales_count"/"sales_amount"
|
||||
- tiers: [{ threshold, mode, value }]
|
||||
- **THEN** 系统创建分配记录并保存梯度配置
|
||||
- **AND** 梯度档位按阈值升序存储
|
||||
|
||||
#### Scenario: 验证梯度档位阈值递增
|
||||
|
||||
- **WHEN** 用户提交的梯度档位阈值不是递增的
|
||||
- **THEN** 系统返回400错误
|
||||
- **AND** 错误消息说明阈值必须递增
|
||||
|
||||
### Requirement: 更新套餐系列分配
|
||||
|
||||
系统 SHALL 允许用户更新现有分配的佣金配置,包括基础返佣和梯度返佣。
|
||||
|
||||
#### Scenario: 更新基础返佣配置
|
||||
|
||||
- **WHEN** 用户提交更新请求,包含新的 base_commission
|
||||
- **THEN** 系统更新分配记录
|
||||
- **AND** 返回更新后的完整信息
|
||||
|
||||
#### Scenario: 启用或禁用梯度返佣
|
||||
|
||||
- **WHEN** 用户更新 enable_tier_commission 标志
|
||||
- **THEN** 系统更新配置
|
||||
- **AND** 如果启用,必须提供有效的 tier_config
|
||||
- **AND** 如果禁用,tier_config 可以为空
|
||||
|
||||
#### Scenario: 更新梯度返佣配置
|
||||
|
||||
- **WHEN** 用户提交包含新 tier_config 的更新请求
|
||||
- **THEN** 系统替换整个梯度配置
|
||||
- **AND** 验证新档位阈值递增
|
||||
- **AND** 返回更新后的信息
|
||||
|
||||
### Requirement: 删除套餐系列分配
|
||||
|
||||
系统 SHALL 允许用户删除套餐系列分配记录。
|
||||
|
||||
#### Scenario: 成功删除分配
|
||||
|
||||
- **WHEN** 用户请求删除指定ID的分配
|
||||
- **THEN** 系统删除该分配记录
|
||||
- **AND** 返回成功响应 (200)
|
||||
|
||||
#### Scenario: 删除不存在的分配
|
||||
|
||||
- **WHEN** 用户请求删除不存在的分配ID
|
||||
- **THEN** 系统返回404错误
|
||||
- **AND** 错误消息说明分配不存在
|
||||
|
||||
### Requirement: 获取套餐系列分配详情
|
||||
|
||||
系统 SHALL 提供单个分配记录的详细信息查询。
|
||||
|
||||
#### Scenario: 成功获取详情
|
||||
|
||||
- **WHEN** 用户请求指定ID的分配详情
|
||||
- **THEN** 系统返回完整的分配信息,包括:
|
||||
- 所有基本字段
|
||||
- base_commission 对象
|
||||
- enable_tier_commission 标志
|
||||
- tier_config (如果启用了梯度返佣)
|
||||
|
||||
### Requirement: 更新套餐系列分配状态
|
||||
|
||||
系统 SHALL 允许用户启用或禁用套餐系列分配。
|
||||
|
||||
#### Scenario: 切换分配状态
|
||||
|
||||
- **WHEN** 用户提交状态更新请求 (status: 1启用/2禁用)
|
||||
- **THEN** 系统更新分配状态
|
||||
- **AND** 返回成功响应
|
||||
|
||||
#### Scenario: 禁用分配后的行为
|
||||
|
||||
- **WHEN** 分配被禁用 (status: 2)
|
||||
- **THEN** 该分配不再参与佣金计算 (由后端业务逻辑保证)
|
||||
- **AND** 前端列表中状态显示为"禁用"
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 定价模式配置
|
||||
|
||||
**Reason**: 旧的定价模式 (pricing_mode, pricing_value) 已被新的佣金模型替代
|
||||
|
||||
**Migration**: 旧数据通过后端迁移脚本转换为基础返佣配置:
|
||||
- pricing_mode="fixed" → base_commission.mode="fixed"
|
||||
- pricing_mode="percentage" → base_commission.mode="percent"
|
||||
- pricing_value → base_commission.value (需要单位转换)
|
||||
|
||||
### Requirement: 一次性佣金配置
|
||||
|
||||
**Reason**: 一次性佣金配置 (one_time_commission_*) 已被梯度返佣系统替代
|
||||
|
||||
**Migration**: 旧的一次性佣金通过以下方式迁移:
|
||||
- 如果设置了一次性佣金,转换为单档位的梯度返佣
|
||||
- trigger → tier_type mapping (first_activation → sales_count, cumulative_recharge → sales_amount)
|
||||
- threshold → tiers[0].threshold
|
||||
- amount → tiers[0].value (mode设为fixed)
|
||||
|
||||
### Requirement: 计算成本价字段
|
||||
|
||||
**Reason**: calculated_cost_price 字段在新模型中不再使用
|
||||
|
||||
**Migration**: 该字段不再返回,成本计算由后端业务逻辑内部处理
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 基础返佣配置
|
||||
|
||||
系统 SHALL 为每个分配提供基础返佣配置,包含返佣模式和返佣值。
|
||||
|
||||
#### Scenario: 固定金额返佣
|
||||
|
||||
- **WHEN** base_commission.mode = "fixed"
|
||||
- **THEN** base_commission.value 表示固定返佣金额(单位:分)
|
||||
- **AND** 每笔交易返佣该固定金额
|
||||
|
||||
#### Scenario: 百分比返佣
|
||||
|
||||
- **WHEN** base_commission.mode = "percent"
|
||||
- **THEN** base_commission.value 表示返佣百分比的千分比 (如200表示20%)
|
||||
- **AND** 每笔交易返佣 = 交易金额 * (value / 1000)
|
||||
|
||||
### Requirement: 梯度返佣配置
|
||||
|
||||
系统 SHALL 支持可选的梯度返佣配置,根据周期内的销量或销售额分档返佣。
|
||||
|
||||
#### Scenario: 按销量分档返佣
|
||||
|
||||
- **WHEN** tier_config.tier_type = "sales_count"
|
||||
- **THEN** 系统根据周期内的销售数量匹配档位
|
||||
- **AND** 达到档位阈值后,该档位的返佣规则生效
|
||||
|
||||
#### Scenario: 按销售额分档返佣
|
||||
|
||||
- **WHEN** tier_config.tier_type = "sales_amount"
|
||||
- **THEN** 系统根据周期内的销售金额(分)匹配档位
|
||||
- **AND** 达到档位阈值后,该档位的返佣规则生效
|
||||
|
||||
#### Scenario: 月度返佣周期
|
||||
|
||||
- **WHEN** tier_config.period_type = "monthly"
|
||||
- **THEN** 系统按自然月统计销量或销售额
|
||||
- **AND** 每月初重置计数
|
||||
|
||||
#### Scenario: 季度返佣周期
|
||||
|
||||
- **WHEN** tier_config.period_type = "quarterly"
|
||||
- **THEN** 系统按季度统计销量或销售额
|
||||
- **AND** 每季度初重置计数
|
||||
|
||||
#### Scenario: 年度返佣周期
|
||||
|
||||
- **WHEN** tier_config.period_type = "yearly"
|
||||
- **THEN** 系统按年度统计销量或销售额
|
||||
- **AND** 每年初重置计数
|
||||
|
||||
### Requirement: 梯度档位管理
|
||||
|
||||
系统 SHALL 支持多个返佣档位,每个档位有独立的阈值和返佣规则。
|
||||
|
||||
#### Scenario: 多档位返佣
|
||||
|
||||
- **WHEN** 配置了多个档位 (tiers数组)
|
||||
- **THEN** 系统按阈值从低到高排列档位
|
||||
- **AND** 当销量/销售额超过某档位阈值时,使用该档位的返佣规则
|
||||
|
||||
#### Scenario: 档位阈值唯一性
|
||||
|
||||
- **WHEN** 用户提交包含重复阈值的档位
|
||||
- **THEN** 系统返回400错误
|
||||
- **AND** 错误消息说明阈值必须唯一
|
||||
|
||||
#### Scenario: 档位返佣模式
|
||||
|
||||
- **WHEN** 档位的 mode = "fixed"
|
||||
- **THEN** value 表示固定返佣金额(分)
|
||||
- **WHEN** 档位的 mode = "percent"
|
||||
- **THEN** value 表示返佣百分比的千分比
|
||||
@@ -0,0 +1,59 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Type Definitions Update
|
||||
|
||||
- [x] 1.1 更新 `ShopSeriesAllocationResponse` 接口,移除旧字段,添加新的佣金字段
|
||||
- [x] 1.2 创建 `BaseCommissionConfig` 类型定义 (mode, value)
|
||||
- [x] 1.3 创建 `TierCommissionConfig` 类型定义 (period_type, tier_type, tiers)
|
||||
- [x] 1.4 创建 `TierEntry` 类型定义 (threshold, mode, value)
|
||||
- [x] 1.5 更新 `CreateShopSeriesAllocationRequest` 接口以支持新的佣金字段
|
||||
- [x] 1.6 更新 `UpdateShopSeriesAllocationRequest` 接口以支持新的佣金字段
|
||||
- [x] 1.7 更新分页响应类型,从 `items` 改为 `list`,添加 `total_pages`
|
||||
|
||||
## 2. API Service Layer Update
|
||||
|
||||
- [x] 2.1 更新 `getShopSeriesAllocations` 方法以处理新的响应结构 (list 而非 items)
|
||||
- [x] 2.2 更新 `createShopSeriesAllocation` 方法以发送新的佣金配置
|
||||
- [x] 2.3 更新 `updateShopSeriesAllocation` 方法以发送新的佣金配置
|
||||
- [x] 2.4 确保所有API方法正确处理新的类型定义
|
||||
|
||||
## 3. Frontend Page Refactoring
|
||||
|
||||
- [x] 3.1 移除旧的定价模式相关代码 (pricing_mode, pricing_value)
|
||||
- [x] 3.2 移除旧的一次性佣金配置相关代码
|
||||
- [x] 3.3 更新表格列定义,移除 `calculated_cost_price` 等旧列
|
||||
- [x] 3.4 添加基础佣金配置表单字段 (mode: fixed/percent, value)
|
||||
- [x] 3.5 添加梯度返佣开关字段 (enable_tier_commission)
|
||||
- [x] 3.6 添加梯度返佣配置表单 (period_type, tier_type, tiers数组)
|
||||
- [x] 3.7 实现梯度档位的动态添加/删除功能
|
||||
- [x] 3.8 更新表格列以显示新的佣金信息 (base_commission)
|
||||
- [x] 3.9 更新表单验证规则以适配新的字段要求
|
||||
- [x] 3.10 更新数据列表获取逻辑,从 `res.data.items` 改为 `res.data.list`
|
||||
- [x] 3.11 更新 `showDialog` 函数以正确填充新的佣金字段
|
||||
- [x] 3.12 更新 `handleDialogClosed` 函数以重置新的佣金字段
|
||||
- [x] 3.13 更新 `handleSubmit` 函数以正确提交新的佣金配置
|
||||
|
||||
## 4. UI/UX Enhancement
|
||||
|
||||
- [x] 4.1 设计并实现基础佣金配置UI (单选模式 + 数值输入)
|
||||
- [x] 4.2 设计并实现梯度返佣配置UI (周期类型、梯度类型、档位列表)
|
||||
- [x] 4.3 添加梯度档位的表单验证 (阈值递增、必填字段等)
|
||||
- [x] 4.4 优化对话框布局以容纳新增的配置项
|
||||
- [x] 4.5 添加梯度返佣的帮助提示或说明文档链接
|
||||
|
||||
## 5. Data Migration Support
|
||||
|
||||
- [x] 5.1 与后端确认数据迁移方案和时间表
|
||||
- [x] 5.2 准备回退方案(如果部署失败)
|
||||
- [x] 5.3 准备测试数据以验证迁移正确性
|
||||
|
||||
## 6. Testing and Validation
|
||||
|
||||
- [x] 6.1 测试基础佣金配置的创建和编辑
|
||||
- [x] 6.2 测试梯度返佣的创建和编辑
|
||||
- [x] 6.3 测试档位添加/删除功能
|
||||
- [x] 6.4 测试表单验证规则
|
||||
- [x] 6.5 测试数据列表的显示和分页
|
||||
- [x] 6.6 测试状态切换功能
|
||||
- [x] 6.7 测试删除功能
|
||||
- [x] 6.8 验证与后端API的集成
|
||||
@@ -61,6 +61,24 @@ export class BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
* @param url 请求URL
|
||||
* @param data 请求数据
|
||||
* @param config 额外配置
|
||||
*/
|
||||
protected static patch<T = any>(
|
||||
url: string,
|
||||
data?: Record<string, any>,
|
||||
config?: Record<string, any>
|
||||
): Promise<T> {
|
||||
return request.patch<T>({
|
||||
url,
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
* @param url 请求URL
|
||||
|
||||
@@ -354,4 +354,17 @@ export class CardService extends BaseService {
|
||||
static getAssetAllocationRecordDetail(id: number): Promise<BaseResponse<any>> {
|
||||
return this.getOne(`/api/admin/asset-allocation-records/${id}`)
|
||||
}
|
||||
|
||||
// ========== 批量设置卡的套餐系列绑定相关 ==========
|
||||
|
||||
/**
|
||||
* 批量设置卡的套餐系列绑定
|
||||
* @param data 请求参数
|
||||
*/
|
||||
static batchSetCardSeriesBinding(data: {
|
||||
iccids: string[]
|
||||
series_allocation_id: number
|
||||
}): Promise<BaseResponse<any>> {
|
||||
return this.patch('/api/admin/iot-cards/series-binding', data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ import type {
|
||||
SubmitWithdrawalParams,
|
||||
ShopCommissionRecordPageResult,
|
||||
ShopCommissionSummaryQueryParams,
|
||||
ShopCommissionSummaryPageResult
|
||||
ShopCommissionSummaryPageResult,
|
||||
MyCommissionStatsQueryParams,
|
||||
MyCommissionStatsResponse,
|
||||
MyDailyCommissionStatsQueryParams,
|
||||
DailyCommissionStatsItem
|
||||
} from '@/types/api/commission'
|
||||
|
||||
export class CommissionService extends BaseService {
|
||||
@@ -130,6 +134,32 @@ export class CommissionService extends BaseService {
|
||||
return this.post<BaseResponse>('/api/admin/my/withdrawal-requests', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的佣金统计
|
||||
* GET /api/admin/my/commission-stats
|
||||
*/
|
||||
static getMyCommissionStats(
|
||||
params?: MyCommissionStatsQueryParams
|
||||
): Promise<BaseResponse<MyCommissionStatsResponse>> {
|
||||
return this.get<BaseResponse<MyCommissionStatsResponse>>(
|
||||
'/api/admin/my/commission-stats',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的每日佣金统计
|
||||
* GET /api/admin/my/commission-daily-stats
|
||||
*/
|
||||
static getMyDailyCommissionStats(
|
||||
params?: MyDailyCommissionStatsQueryParams
|
||||
): Promise<BaseResponse<DailyCommissionStatsItem[]>> {
|
||||
return this.get<BaseResponse<DailyCommissionStatsItem[]>>(
|
||||
'/api/admin/my/commission-daily-stats',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 代理商佣金管理 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -154,4 +154,17 @@ export class DeviceService extends BaseService {
|
||||
static getImportTaskDetail(id: number): Promise<BaseResponse<DeviceImportTaskDetail>> {
|
||||
return this.getOne<DeviceImportTaskDetail>(`/api/admin/devices/import/tasks/${id}`)
|
||||
}
|
||||
|
||||
// ========== 批量设置设备的套餐系列绑定相关 ==========
|
||||
|
||||
/**
|
||||
* 批量设置设备的套餐系列绑定
|
||||
* @param data 请求参数
|
||||
*/
|
||||
static batchSetDeviceSeriesBinding(data: {
|
||||
device_ids: number[]
|
||||
series_allocation_id: number
|
||||
}): Promise<BaseResponse<any>> {
|
||||
return this.patch('/api/admin/devices/series-binding', data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ import type {
|
||||
RecallCardsRequest,
|
||||
RecallCardsResponse
|
||||
} from '@/types/api/enterpriseCard'
|
||||
import type {
|
||||
EnterpriseDeviceListParams,
|
||||
EnterpriseDevicePageResult,
|
||||
AllocateDevicesRequest,
|
||||
AllocateDevicesResponse,
|
||||
RecallDevicesRequest,
|
||||
RecallDevicesResponse
|
||||
} from '@/types/api/enterpriseDevice'
|
||||
|
||||
export class EnterpriseService extends BaseService {
|
||||
/**
|
||||
@@ -157,4 +165,51 @@ export class EnterpriseService extends BaseService {
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
// ========== 企业设备授权相关 ==========
|
||||
|
||||
/**
|
||||
* 授权设备给企业
|
||||
* @param enterpriseId 企业ID
|
||||
* @param data 授权请求数据
|
||||
*/
|
||||
static allocateDevices(
|
||||
enterpriseId: number,
|
||||
data: AllocateDevicesRequest
|
||||
): Promise<BaseResponse<AllocateDevicesResponse>> {
|
||||
return this.post<BaseResponse<AllocateDevicesResponse>>(
|
||||
`/api/admin/enterprises/${enterpriseId}/allocate-devices`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业设备列表
|
||||
* @param enterpriseId 企业ID
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getEnterpriseDevices(
|
||||
enterpriseId: number,
|
||||
params?: EnterpriseDeviceListParams
|
||||
): Promise<BaseResponse<EnterpriseDevicePageResult>> {
|
||||
return this.get<BaseResponse<EnterpriseDevicePageResult>>(
|
||||
`/api/admin/enterprises/${enterpriseId}/devices`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销设备授权
|
||||
* @param enterpriseId 企业ID
|
||||
* @param data 撤销请求数据
|
||||
*/
|
||||
static recallDevices(
|
||||
enterpriseId: number,
|
||||
data: RecallDevicesRequest
|
||||
): Promise<BaseResponse<RecallDevicesResponse>> {
|
||||
return this.post<BaseResponse<RecallDevicesResponse>>(
|
||||
`/api/admin/enterprises/${enterpriseId}/recall-devices`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ export { StorageService } from './storage'
|
||||
export { AuthorizationService } from './authorization'
|
||||
export { DeviceService } from './device'
|
||||
export { CarrierService } from './carrier'
|
||||
export { PackageSeriesService } from './packageSeries'
|
||||
export { PackageManageService } from './packageManage'
|
||||
export { MyPackageService } from './myPackage'
|
||||
export { ShopPackageAllocationService } from './shopPackageAllocation'
|
||||
export { ShopSeriesAllocationService } from './shopSeriesAllocation'
|
||||
export { OrderService } from './order'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { PackageService } from './package'
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
45
src/api/modules/myPackage.ts
Normal file
45
src/api/modules/myPackage.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 代理可售套餐 API 服务
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
MyPackageResponse,
|
||||
MyPackageQueryParams,
|
||||
MySeriesAllocationResponse,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class MyPackageService extends BaseService {
|
||||
/**
|
||||
* 获取我的可售套餐列表
|
||||
* GET /api/admin/my-packages
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getMyPackages(
|
||||
params?: MyPackageQueryParams
|
||||
): Promise<PaginationResponse<MyPackageResponse>> {
|
||||
return this.getPage<MyPackageResponse>('/api/admin/my-packages', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的可售套餐详情
|
||||
* GET /api/admin/my-packages/{id}
|
||||
* @param id 套餐ID
|
||||
*/
|
||||
static getMyPackageDetail(id: number): Promise<BaseResponse<MyPackageResponse>> {
|
||||
return this.getOne<MyPackageResponse>(`/api/admin/my-packages/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的被分配系列列表
|
||||
* GET /api/admin/my-series-allocations
|
||||
* @param params 查询参数(支持分页)
|
||||
*/
|
||||
static getMySeriesAllocations(
|
||||
params?: Record<string, any>
|
||||
): Promise<PaginationResponse<MySeriesAllocationResponse>> {
|
||||
return this.getPage<MySeriesAllocationResponse>('/api/admin/my-series-allocations', params)
|
||||
}
|
||||
}
|
||||
47
src/api/modules/order.ts
Normal file
47
src/api/modules/order.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 订单管理相关 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
Order,
|
||||
OrderQueryParams,
|
||||
OrderListResponse,
|
||||
CreateOrderRequest,
|
||||
CreateOrderResponse,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class OrderService extends BaseService {
|
||||
/**
|
||||
* 获取订单列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getOrders(params?: OrderQueryParams): Promise<BaseResponse<OrderListResponse>> {
|
||||
return this.get<BaseResponse<OrderListResponse>>('/api/admin/orders', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
* @param id 订单ID
|
||||
*/
|
||||
static getOrderById(id: number): Promise<BaseResponse<Order>> {
|
||||
return this.getOne<Order>(`/api/admin/orders/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param data 创建订单请求参数
|
||||
*/
|
||||
static createOrder(data: CreateOrderRequest): Promise<BaseResponse<CreateOrderResponse>> {
|
||||
return this.post<BaseResponse<CreateOrderResponse>>('/api/admin/orders', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @param id 订单ID
|
||||
*/
|
||||
static cancelOrder(id: number): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/orders/${id}/cancel`, {})
|
||||
}
|
||||
}
|
||||
91
src/api/modules/packageManage.ts
Normal file
91
src/api/modules/packageManage.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 套餐管理 API 服务
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
PackageResponse,
|
||||
PackageQueryParams,
|
||||
CreatePackageRequest,
|
||||
UpdatePackageRequest,
|
||||
UpdatePackageStatusRequest,
|
||||
UpdatePackageShelfStatusRequest,
|
||||
SeriesSelectOption,
|
||||
BaseResponse,
|
||||
PaginationResponse,
|
||||
ListResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class PackageManageService extends BaseService {
|
||||
/**
|
||||
* 获取套餐分页列表
|
||||
* GET /api/admin/packages
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getPackages(params?: PackageQueryParams): Promise<PaginationResponse<PackageResponse>> {
|
||||
return this.getPage<PackageResponse>('/api/admin/packages', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐
|
||||
* POST /api/admin/packages
|
||||
* @param data 套餐数据
|
||||
*/
|
||||
static createPackage(data: CreatePackageRequest): Promise<BaseResponse<PackageResponse>> {
|
||||
return this.create<PackageResponse>('/api/admin/packages', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐详情
|
||||
* GET /api/admin/packages/{id}
|
||||
* @param id 套餐ID
|
||||
*/
|
||||
static getPackageDetail(id: number): Promise<BaseResponse<PackageResponse>> {
|
||||
return this.getOne<PackageResponse>(`/api/admin/packages/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐
|
||||
* PUT /api/admin/packages/{id}
|
||||
* @param id 套餐ID
|
||||
* @param data 套餐数据
|
||||
*/
|
||||
static updatePackage(
|
||||
id: number,
|
||||
data: UpdatePackageRequest
|
||||
): Promise<BaseResponse<PackageResponse>> {
|
||||
return this.update<PackageResponse>(`/api/admin/packages/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除套餐
|
||||
* DELETE /api/admin/packages/{id}
|
||||
* @param id 套餐ID
|
||||
*/
|
||||
static deletePackage(id: number): Promise<BaseResponse> {
|
||||
return this.remove(`/api/admin/packages/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐状态
|
||||
* PUT /api/admin/packages/{id}/status
|
||||
* @param id 套餐ID
|
||||
* @param status 状态 (1:启用, 2:禁用)
|
||||
*/
|
||||
static updatePackageStatus(id: number, status: number): Promise<BaseResponse> {
|
||||
const data: UpdatePackageStatusRequest = { status }
|
||||
return this.put<BaseResponse>(`/api/admin/packages/${id}/status`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐上架状态
|
||||
* PATCH /api/admin/packages/{id}/shelf
|
||||
* @param id 套餐ID
|
||||
* @param shelf_status 上架状态 (1:上架, 2:下架)
|
||||
*/
|
||||
static updatePackageShelfStatus(id: number, shelf_status: number): Promise<BaseResponse> {
|
||||
const data: UpdatePackageShelfStatusRequest = { shelf_status }
|
||||
return this.patch<BaseResponse>(`/api/admin/packages/${id}/shelf`, data)
|
||||
}
|
||||
|
||||
}
|
||||
83
src/api/modules/packageSeries.ts
Normal file
83
src/api/modules/packageSeries.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 套餐系列管理 API 服务
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
PackageSeriesResponse,
|
||||
PackageSeriesQueryParams,
|
||||
CreatePackageSeriesRequest,
|
||||
UpdatePackageSeriesRequest,
|
||||
UpdatePackageSeriesStatusRequest,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class PackageSeriesService extends BaseService {
|
||||
/**
|
||||
* 获取套餐系列分页列表
|
||||
* GET /api/admin/package-series
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getPackageSeries(
|
||||
params?: PackageSeriesQueryParams
|
||||
): Promise<PaginationResponse<PackageSeriesResponse>> {
|
||||
return this.getPage<PackageSeriesResponse>('/api/admin/package-series', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐系列
|
||||
* POST /api/admin/package-series
|
||||
* @param data 套餐系列数据
|
||||
*/
|
||||
static createPackageSeries(
|
||||
data: CreatePackageSeriesRequest
|
||||
): Promise<BaseResponse<PackageSeriesResponse>> {
|
||||
return this.create<PackageSeriesResponse>('/api/admin/package-series', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐系列详情
|
||||
* GET /api/admin/package-series/{id}
|
||||
* @param id 系列ID
|
||||
*/
|
||||
static getPackageSeriesDetail(id: number): Promise<BaseResponse<PackageSeriesResponse>> {
|
||||
return this.getOne<PackageSeriesResponse>(`/api/admin/package-series/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列
|
||||
* PUT /api/admin/package-series/{id}
|
||||
* @param id 系列ID
|
||||
* @param data 套餐系列数据
|
||||
*/
|
||||
static updatePackageSeries(
|
||||
id: number,
|
||||
data: UpdatePackageSeriesRequest
|
||||
): Promise<BaseResponse<PackageSeriesResponse>> {
|
||||
return this.update<PackageSeriesResponse>(`/api/admin/package-series/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除套餐系列
|
||||
* DELETE /api/admin/package-series/{id}
|
||||
* @param id 系列ID
|
||||
*/
|
||||
static deletePackageSeries(id: number): Promise<BaseResponse> {
|
||||
return this.remove(`/api/admin/package-series/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列状态
|
||||
* PUT /api/admin/package-series/{id}/status
|
||||
* @param id 系列ID
|
||||
* @param status 状态 (1:启用, 2:禁用)
|
||||
*/
|
||||
static updatePackageSeriesStatus(
|
||||
id: number,
|
||||
status: number
|
||||
): Promise<BaseResponse> {
|
||||
const data: UpdatePackageSeriesStatusRequest = { status }
|
||||
return this.put<BaseResponse>(`/api/admin/package-series/${id}/status`, data)
|
||||
}
|
||||
}
|
||||
109
src/api/modules/shopPackageAllocation.ts
Normal file
109
src/api/modules/shopPackageAllocation.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 单套餐分配 API 服务
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
ShopPackageAllocationResponse,
|
||||
ShopPackageAllocationQueryParams,
|
||||
CreateShopPackageAllocationRequest,
|
||||
UpdateShopPackageAllocationRequest,
|
||||
UpdateShopPackageAllocationStatusRequest,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class ShopPackageAllocationService extends BaseService {
|
||||
/**
|
||||
* 获取单套餐分配列表
|
||||
* GET /api/admin/shop-package-allocations
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getShopPackageAllocations(
|
||||
params?: ShopPackageAllocationQueryParams
|
||||
): Promise<PaginationResponse<ShopPackageAllocationResponse>> {
|
||||
return this.getPage<ShopPackageAllocationResponse>(
|
||||
'/api/admin/shop-package-allocations',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单套餐分配
|
||||
* POST /api/admin/shop-package-allocations
|
||||
* @param data 分配数据
|
||||
*/
|
||||
static createShopPackageAllocation(
|
||||
data: CreateShopPackageAllocationRequest
|
||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||
return this.create<ShopPackageAllocationResponse>(
|
||||
'/api/admin/shop-package-allocations',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单套餐分配详情
|
||||
* GET /api/admin/shop-package-allocations/{id}
|
||||
* @param id 分配ID
|
||||
*/
|
||||
static getShopPackageAllocationDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||
return this.getOne<ShopPackageAllocationResponse>(
|
||||
`/api/admin/shop-package-allocations/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单套餐分配
|
||||
* PUT /api/admin/shop-package-allocations/{id}
|
||||
* @param id 分配ID
|
||||
* @param data 分配数据(只允许修改成本价)
|
||||
*/
|
||||
static updateShopPackageAllocation(
|
||||
id: number,
|
||||
data: UpdateShopPackageAllocationRequest
|
||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||
return this.update<ShopPackageAllocationResponse>(
|
||||
`/api/admin/shop-package-allocations/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单套餐分配
|
||||
* DELETE /api/admin/shop-package-allocations/{id}
|
||||
* @param id 分配ID
|
||||
*/
|
||||
static deleteShopPackageAllocation(id: number): Promise<BaseResponse> {
|
||||
return this.remove(`/api/admin/shop-package-allocations/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单套餐分配成本价
|
||||
* PUT /api/admin/shop-package-allocations/{id}/cost-price
|
||||
* @param id 分配ID
|
||||
* @param costPrice 成本价(分)
|
||||
*/
|
||||
static updateShopPackageAllocationCostPrice(
|
||||
id: number,
|
||||
costPrice: number
|
||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||
return this.put<BaseResponse<ShopPackageAllocationResponse>>(
|
||||
`/api/admin/shop-package-allocations/${id}/cost-price`,
|
||||
{ cost_price: costPrice }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单套餐分配状态
|
||||
* PUT /api/admin/shop-package-allocations/{id}/status
|
||||
* @param id 分配ID
|
||||
* @param status 状态 (1:启用, 2:禁用)
|
||||
*/
|
||||
static updateShopPackageAllocationStatus(id: number, status: number): Promise<BaseResponse> {
|
||||
const data: UpdateShopPackageAllocationStatusRequest = { status }
|
||||
return this.put<BaseResponse>(`/api/admin/shop-package-allocations/${id}/status`, data)
|
||||
}
|
||||
}
|
||||
88
src/api/modules/shopSeriesAllocation.ts
Normal file
88
src/api/modules/shopSeriesAllocation.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 套餐系列分配 API 服务
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
ShopSeriesAllocationResponse,
|
||||
ShopSeriesAllocationQueryParams,
|
||||
CreateShopSeriesAllocationRequest,
|
||||
UpdateShopSeriesAllocationRequest,
|
||||
UpdateShopSeriesAllocationStatusRequest,
|
||||
BaseResponse,
|
||||
PaginationResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class ShopSeriesAllocationService extends BaseService {
|
||||
/**
|
||||
* 获取套餐系列分配分页列表
|
||||
* GET /api/admin/shop-series-allocations
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getShopSeriesAllocations(
|
||||
params?: ShopSeriesAllocationQueryParams
|
||||
): Promise<PaginationResponse<ShopSeriesAllocationResponse>> {
|
||||
return this.getPage<ShopSeriesAllocationResponse>(
|
||||
'/api/admin/shop-series-allocations',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐系列分配
|
||||
* POST /api/admin/shop-series-allocations
|
||||
* @param data 分配数据
|
||||
*/
|
||||
static createShopSeriesAllocation(
|
||||
data: CreateShopSeriesAllocationRequest
|
||||
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
|
||||
return this.create<ShopSeriesAllocationResponse>('/api/admin/shop-series-allocations', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐系列分配详情
|
||||
* GET /api/admin/shop-series-allocations/{id}
|
||||
* @param id 分配ID
|
||||
*/
|
||||
static getShopSeriesAllocationDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
|
||||
return this.getOne<ShopSeriesAllocationResponse>(`/api/admin/shop-series-allocations/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列分配
|
||||
* PUT /api/admin/shop-series-allocations/{id}
|
||||
* @param id 分配ID
|
||||
* @param data 分配数据
|
||||
*/
|
||||
static updateShopSeriesAllocation(
|
||||
id: number,
|
||||
data: UpdateShopSeriesAllocationRequest
|
||||
): Promise<BaseResponse<ShopSeriesAllocationResponse>> {
|
||||
return this.update<ShopSeriesAllocationResponse>(
|
||||
`/api/admin/shop-series-allocations/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除套餐系列分配
|
||||
* DELETE /api/admin/shop-series-allocations/{id}
|
||||
* @param id 分配ID
|
||||
*/
|
||||
static deleteShopSeriesAllocation(id: number): Promise<BaseResponse> {
|
||||
return this.remove(`/api/admin/shop-series-allocations/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列分配状态
|
||||
* PUT /api/admin/shop-series-allocations/{id}/status
|
||||
* @param id 分配ID
|
||||
* @param status 状态 (1:启用, 2:禁用)
|
||||
*/
|
||||
static updateShopSeriesAllocationStatus(id: number, status: number): Promise<BaseResponse> {
|
||||
const data: UpdateShopSeriesAllocationStatusRequest = { status }
|
||||
return this.put<BaseResponse>(`/api/admin/shop-series-allocations/${id}/status`, data)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@
|
||||
<div class="user-info-display">
|
||||
<div class="avatar">{{ getUserAvatar }}</div>
|
||||
<div class="info">
|
||||
<div class="username">{{ userInfo.username || '用户' }}</div>
|
||||
<div class="username">{{ userInfo.username || '' }}</div>
|
||||
<div class="user-type">{{ userInfo.user_type_name || '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +243,7 @@
|
||||
*/
|
||||
const getUserAvatar = computed(() => {
|
||||
const username = userInfo.value.username
|
||||
if (!username) return 'U'
|
||||
if (!username) return ''
|
||||
|
||||
const firstChar = username.charAt(0)
|
||||
// 检查是否为中文字符(Unicode 范围:\u4e00-\u9fa5)
|
||||
|
||||
@@ -22,3 +22,6 @@ export * from './iotCard'
|
||||
|
||||
// 运营商类型相关
|
||||
export * from './carrierTypes'
|
||||
|
||||
// 套餐管理相关
|
||||
export * from './package'
|
||||
|
||||
293
src/config/constants/package.ts
Normal file
293
src/config/constants/package.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 套餐管理相关常量配置
|
||||
*/
|
||||
|
||||
// ==================== 套餐类型 ====================
|
||||
|
||||
/**
|
||||
* 套餐类型枚举
|
||||
*/
|
||||
export enum PackageType {
|
||||
FORMAL = 'formal', // 正式套餐
|
||||
ADDON = 'addon' // 加油包
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐类型选项
|
||||
*/
|
||||
export const PACKAGE_TYPE_OPTIONS = [
|
||||
{
|
||||
label: '正式套餐',
|
||||
value: PackageType.FORMAL,
|
||||
type: 'primary' as const
|
||||
},
|
||||
{
|
||||
label: '加油包',
|
||||
value: PackageType.ADDON,
|
||||
type: 'success' as const
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 套餐类型映射
|
||||
*/
|
||||
export const PACKAGE_TYPE_MAP = PACKAGE_TYPE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<PackageType, { label: string; value: PackageType; type: 'primary' | 'success' }>
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取套餐类型标签
|
||||
*/
|
||||
export function getPackageTypeLabel(type: string): string {
|
||||
return PACKAGE_TYPE_MAP[type as PackageType]?.label || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐类型标签类型
|
||||
*/
|
||||
export function getPackageTypeTag(type: string) {
|
||||
return PACKAGE_TYPE_MAP[type as PackageType]?.type || 'info'
|
||||
}
|
||||
|
||||
// ==================== 流量类型 ====================
|
||||
|
||||
/**
|
||||
* 流量类型枚举
|
||||
*/
|
||||
export enum DataType {
|
||||
REAL = 'real', // 真实流量
|
||||
VIRTUAL = 'virtual' // 虚拟流量
|
||||
}
|
||||
|
||||
/**
|
||||
* 流量类型选项
|
||||
*/
|
||||
export const DATA_TYPE_OPTIONS = [
|
||||
{
|
||||
label: '真实流量',
|
||||
value: DataType.REAL,
|
||||
type: 'success' as const
|
||||
},
|
||||
{
|
||||
label: '虚拟流量',
|
||||
value: DataType.VIRTUAL,
|
||||
type: 'warning' as const
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 流量类型映射
|
||||
*/
|
||||
export const DATA_TYPE_MAP = DATA_TYPE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<DataType, { label: string; value: DataType; type: 'success' | 'warning' }>
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取流量类型标签
|
||||
*/
|
||||
export function getDataTypeLabel(type: string): string {
|
||||
return DATA_TYPE_MAP[type as DataType]?.label || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量类型标签类型
|
||||
*/
|
||||
export function getDataTypeTag(type: string) {
|
||||
return DATA_TYPE_MAP[type as DataType]?.type || 'info'
|
||||
}
|
||||
|
||||
// ==================== 上架状态 ====================
|
||||
|
||||
/**
|
||||
* 上架状态枚举
|
||||
*/
|
||||
export enum ShelfStatus {
|
||||
ON_SHELF = 1, // 上架
|
||||
OFF_SHELF = 2 // 下架
|
||||
}
|
||||
|
||||
/**
|
||||
* 上架状态选项
|
||||
*/
|
||||
export const SHELF_STATUS_OPTIONS = [
|
||||
{
|
||||
label: '上架',
|
||||
value: ShelfStatus.ON_SHELF,
|
||||
type: 'success' as const,
|
||||
text: '上架'
|
||||
},
|
||||
{
|
||||
label: '下架',
|
||||
value: ShelfStatus.OFF_SHELF,
|
||||
type: 'info' as const,
|
||||
text: '下架'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 上架状态映射
|
||||
*/
|
||||
export const SHELF_STATUS_MAP = SHELF_STATUS_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<
|
||||
ShelfStatus,
|
||||
{ label: string; value: ShelfStatus; type: 'success' | 'info'; text: string }
|
||||
>
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取上架状态标签
|
||||
*/
|
||||
export function getShelfStatusLabel(status: number): string {
|
||||
return SHELF_STATUS_MAP[status as ShelfStatus]?.label || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上架状态文本
|
||||
*/
|
||||
export function getShelfStatusText(status: number): string {
|
||||
return SHELF_STATUS_MAP[status as ShelfStatus]?.text || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上架状态标签类型
|
||||
*/
|
||||
export function getShelfStatusType(status: number) {
|
||||
return SHELF_STATUS_MAP[status as ShelfStatus]?.type || 'info'
|
||||
}
|
||||
|
||||
// ==================== 定价模式 ====================
|
||||
|
||||
/**
|
||||
* 定价模式枚举
|
||||
*/
|
||||
export enum PricingMode {
|
||||
FIXED = 'fixed', // 固定金额
|
||||
PERCENT = 'percent' // 百分比
|
||||
}
|
||||
|
||||
/**
|
||||
* 定价模式选项
|
||||
*/
|
||||
export const PRICING_MODE_OPTIONS = [
|
||||
{
|
||||
label: '固定金额',
|
||||
value: PricingMode.FIXED,
|
||||
type: 'primary' as const
|
||||
},
|
||||
{
|
||||
label: '百分比',
|
||||
value: PricingMode.PERCENT,
|
||||
type: 'success' as const
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 定价模式映射
|
||||
*/
|
||||
export const PRICING_MODE_MAP = PRICING_MODE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<PricingMode, { label: string; value: PricingMode; type: 'primary' | 'success' }>
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取定价模式标签
|
||||
*/
|
||||
export function getPricingModeLabel(mode: string): string {
|
||||
return PRICING_MODE_MAP[mode as PricingMode]?.label || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定价模式标签类型
|
||||
*/
|
||||
export function getPricingModeTag(mode: string) {
|
||||
return PRICING_MODE_MAP[mode as PricingMode]?.type || 'info'
|
||||
}
|
||||
|
||||
// ==================== 价格来源 ====================
|
||||
|
||||
/**
|
||||
* 价格来源枚举
|
||||
*/
|
||||
export enum PriceSource {
|
||||
SERIES_PRICING = 'series_pricing', // 系列加价
|
||||
PACKAGE_OVERRIDE = 'package_override' // 单套餐覆盖
|
||||
}
|
||||
|
||||
/**
|
||||
* 价格来源选项
|
||||
*/
|
||||
export const PRICE_SOURCE_OPTIONS = [
|
||||
{
|
||||
label: '系列加价',
|
||||
value: PriceSource.SERIES_PRICING,
|
||||
type: 'primary' as const
|
||||
},
|
||||
{
|
||||
label: '单套餐覆盖',
|
||||
value: PriceSource.PACKAGE_OVERRIDE,
|
||||
type: 'warning' as const
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 价格来源映射
|
||||
*/
|
||||
export const PRICE_SOURCE_MAP = PRICE_SOURCE_OPTIONS.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item
|
||||
return map
|
||||
},
|
||||
{} as Record<
|
||||
PriceSource,
|
||||
{ label: string; value: PriceSource; type: 'primary' | 'warning' }
|
||||
>
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取价格来源标签
|
||||
*/
|
||||
export function getPriceSourceLabel(source: string): string {
|
||||
return PRICE_SOURCE_MAP[source as PriceSource]?.label || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取价格来源标签类型
|
||||
*/
|
||||
export function getPriceSourceTag(source: string) {
|
||||
return PRICE_SOURCE_MAP[source as PriceSource]?.type || 'info'
|
||||
}
|
||||
|
||||
// ==================== 状态值映射工具 ====================
|
||||
|
||||
/**
|
||||
* 前端状态值 (CommonStatus: 0/1) 转换为后端 API 状态值 (1/2)
|
||||
* @param frontendStatus 前端状态值 (0:禁用, 1:启用)
|
||||
* @returns 后端状态值 (1:启用, 2:禁用)
|
||||
*/
|
||||
export function frontendStatusToApi(frontendStatus: number): number {
|
||||
return frontendStatus === 1 ? 1 : 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端 API 状态值 (1/2) 转换为前端状态值 (CommonStatus: 0/1)
|
||||
* @param apiStatus 后端状态值 (1:启用, 2:禁用)
|
||||
* @returns 前端状态值 (0:禁用, 1:启用)
|
||||
*/
|
||||
export function apiStatusToFrontend(apiStatus: number): number {
|
||||
return apiStatus === 1 ? 1 : 0
|
||||
}
|
||||
@@ -392,6 +392,7 @@
|
||||
"packageList": "My Packages",
|
||||
"packageChange": "Package Change",
|
||||
"packageAssign": "Package Assignment",
|
||||
"seriesAssign": "Series Assignment",
|
||||
"packageSeries": "Package Series",
|
||||
"packageCommission": "Package Commission Cards"
|
||||
},
|
||||
@@ -414,7 +415,8 @@
|
||||
"withdrawal": "Commission Withdrawal",
|
||||
"withdrawalSettings": "Withdrawal Settings",
|
||||
"myAccount": "My Account",
|
||||
"carrierManagement": "Carrier Management"
|
||||
"carrierManagement": "Carrier Management",
|
||||
"orders": "Order Management"
|
||||
},
|
||||
"deviceManagement": {
|
||||
"title": "Device Management",
|
||||
@@ -443,7 +445,8 @@
|
||||
"allocationRecordDetail": "Allocation Record Details",
|
||||
"cardReplacementRequest": "Card Replacement Request",
|
||||
"authorizationRecords": "Authorization Records",
|
||||
"authorizationDetail": "Authorization Details"
|
||||
"authorizationDetail": "Authorization Details",
|
||||
"enterpriseDevices": "Enterprise Devices"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings Management",
|
||||
@@ -741,5 +744,206 @@
|
||||
"pendingAmount": "Pending Amount",
|
||||
"todayCommission": "Today's Commission"
|
||||
}
|
||||
},
|
||||
"seriesBinding": {
|
||||
"buttons": {
|
||||
"batchSetSeries": "Batch Set Series",
|
||||
"clearBinding": "Clear Binding"
|
||||
},
|
||||
"dialog": {
|
||||
"title": "Batch Set Series Binding",
|
||||
"titleCard": "Batch Set Card Series Binding",
|
||||
"titleDevice": "Batch Set Device Series Binding"
|
||||
},
|
||||
"form": {
|
||||
"seriesAllocation": "Series Allocation",
|
||||
"seriesAllocationPlaceholder": "Please select series allocation",
|
||||
"clearBindingOption": "Clear Association",
|
||||
"selectedCount": "{count} items selected"
|
||||
},
|
||||
"messages": {
|
||||
"noSelection": "Please select items to set",
|
||||
"setSuccess": "Series binding set successfully",
|
||||
"setFailed": "Failed to set series binding",
|
||||
"partialSuccess": "Partial success: {success} succeeded, {fail} failed",
|
||||
"confirmSet": "Are you sure to set series binding for {count} selected items?",
|
||||
"confirmClear": "Are you sure to clear series binding for {count} selected items?"
|
||||
},
|
||||
"result": {
|
||||
"title": "Set Result",
|
||||
"successCount": "Success Count",
|
||||
"failCount": "Fail Count",
|
||||
"failedItems": "Failed Items",
|
||||
"iccid": "ICCID",
|
||||
"deviceId": "Device ID",
|
||||
"deviceNo": "Device No.",
|
||||
"reason": "Reason"
|
||||
}
|
||||
},
|
||||
"orderManagement": {
|
||||
"title": "Order Management",
|
||||
"orderList": "Order List",
|
||||
"orderDetail": "Order Details",
|
||||
"orderItems": "Order Items",
|
||||
"createOrder": "Create Order",
|
||||
"searchForm": {
|
||||
"orderNo": "Order No.",
|
||||
"orderNoPlaceholder": "Please enter order number",
|
||||
"paymentStatus": "Payment Status",
|
||||
"paymentStatusPlaceholder": "Please select payment status",
|
||||
"orderType": "Order Type",
|
||||
"orderTypePlaceholder": "Please select order type",
|
||||
"dateRange": "Created Time",
|
||||
"startDate": "Start Time",
|
||||
"endDate": "End Time"
|
||||
},
|
||||
"table": {
|
||||
"id": "Order ID",
|
||||
"orderNo": "Order No.",
|
||||
"orderType": "Order Type",
|
||||
"buyerId": "Buyer ID",
|
||||
"buyerType": "Buyer Type",
|
||||
"paymentStatus": "Payment Status",
|
||||
"paymentMethod": "Payment Method",
|
||||
"totalAmount": "Total Amount",
|
||||
"paidAt": "Paid At",
|
||||
"commissionStatus": "Commission Status",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"operation": "Actions"
|
||||
},
|
||||
"orderType": {
|
||||
"singleCard": "Single Card Purchase",
|
||||
"device": "Device Purchase"
|
||||
},
|
||||
"buyerType": {
|
||||
"personal": "Personal Customer",
|
||||
"agent": "Agent"
|
||||
},
|
||||
"paymentStatus": {
|
||||
"pending": "Pending",
|
||||
"paid": "Paid",
|
||||
"cancelled": "Cancelled",
|
||||
"refunded": "Refunded"
|
||||
},
|
||||
"paymentMethod": {
|
||||
"wallet": "Wallet Payment",
|
||||
"wechat": "WeChat Payment",
|
||||
"alipay": "Alipay Payment"
|
||||
},
|
||||
"commissionStatus": {
|
||||
"notApplicable": "Not Applicable",
|
||||
"pending": "Pending",
|
||||
"settled": "Settled"
|
||||
},
|
||||
"actions": {
|
||||
"viewDetail": "View Details",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"submit": "Submit",
|
||||
"close": "Close"
|
||||
},
|
||||
"createForm": {
|
||||
"packageIds": "Select Packages",
|
||||
"packageIdsPlaceholder": "Please select packages",
|
||||
"iotCardId": "IoT Card ID",
|
||||
"iotCardIdPlaceholder": "Please enter IoT card ID",
|
||||
"deviceId": "Device ID",
|
||||
"deviceIdPlaceholder": "Please enter device ID"
|
||||
},
|
||||
"items": {
|
||||
"packageName": "Package Name",
|
||||
"quantity": "Quantity",
|
||||
"unitPrice": "Unit Price",
|
||||
"amount": "Subtotal"
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "Order created successfully",
|
||||
"createFailed": "Failed to create order",
|
||||
"cancelSuccess": "Order cancelled successfully",
|
||||
"cancelFailed": "Failed to cancel order",
|
||||
"cancelConfirm": "Cancel Order Confirmation",
|
||||
"cancelConfirmText": "Are you sure to cancel this order? This action cannot be undone",
|
||||
"cannotCancelPaid": "Cannot cancel a paid order",
|
||||
"cannotCancelCancelled": "Order is already cancelled",
|
||||
"cannotCancelRefunded": "Cannot cancel a refunded order",
|
||||
"loadFailed": "Failed to load order data",
|
||||
"noData": "No order data available"
|
||||
},
|
||||
"validation": {
|
||||
"orderTypeRequired": "Please select order type",
|
||||
"packageIdsRequired": "Please select at least one package",
|
||||
"iotCardRequired": "Please select IoT card",
|
||||
"deviceRequired": "Please select device",
|
||||
"packagesMaxLimit": "Can select up to 10 packages"
|
||||
}
|
||||
},
|
||||
"enterpriseDevices": {
|
||||
"title": "Enterprise Device List",
|
||||
"searchForm": {
|
||||
"enterpriseId": "Enterprise ID",
|
||||
"enterpriseIdPlaceholder": "Please enter enterprise ID",
|
||||
"deviceNo": "Device No.",
|
||||
"deviceNoPlaceholder": "Please enter device number"
|
||||
},
|
||||
"table": {
|
||||
"deviceId": "Device ID",
|
||||
"deviceNo": "Device No.",
|
||||
"deviceName": "Device Name",
|
||||
"deviceModel": "Device Model",
|
||||
"cardCount": "Card Count",
|
||||
"authorizedAt": "Authorized At",
|
||||
"operation": "Actions"
|
||||
},
|
||||
"buttons": {
|
||||
"allocateDevices": "Allocate Devices",
|
||||
"recallDevices": "Recall Authorization",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"dialog": {
|
||||
"allocateTitle": "Allocate Devices to Enterprise",
|
||||
"recallTitle": "Recall Device Authorization",
|
||||
"resultTitle": "Operation Result"
|
||||
},
|
||||
"form": {
|
||||
"deviceNos": "Device Numbers",
|
||||
"deviceNosPlaceholder": "Enter device numbers, separated by newlines or commas",
|
||||
"deviceNosHint": "One device number per line or comma-separated, maximum 100 devices",
|
||||
"remark": "Remark",
|
||||
"remarkPlaceholder": "Enter allocation remark (optional)",
|
||||
"selectedDevices": "Selected Devices",
|
||||
"selectedCount": "{count} devices selected"
|
||||
},
|
||||
"result": {
|
||||
"successCount": "Success Count",
|
||||
"failCount": "Fail Count",
|
||||
"authorizedDevices": "Authorized Devices",
|
||||
"failedItems": "Failed Items",
|
||||
"deviceNo": "Device No.",
|
||||
"deviceId": "Device ID",
|
||||
"cardCount": "Card Count",
|
||||
"reason": "Failure Reason"
|
||||
},
|
||||
"messages": {
|
||||
"allocateSuccess": "Device allocation successful",
|
||||
"allocateFailed": "Device allocation failed",
|
||||
"allocatePartialSuccess": "Partial success: {success} succeeded, {fail} failed",
|
||||
"recallSuccess": "Authorization recall successful",
|
||||
"recallFailed": "Authorization recall failed",
|
||||
"recallPartialSuccess": "Partial recall success: {success} succeeded, {fail} failed",
|
||||
"recallConfirm": "Recall Authorization Confirmation",
|
||||
"recallConfirmText": "Are you sure to recall authorization for {count} selected devices?",
|
||||
"noSelection": "Please select devices to recall first",
|
||||
"deviceNosRequired": "Please enter device numbers",
|
||||
"deviceNosEmpty": "Device number list cannot be empty",
|
||||
"deviceNosMaxLimit": "Cannot exceed 100 device numbers",
|
||||
"invalidDeviceNos": "Invalid device number format exists",
|
||||
"loadFailed": "Failed to load device list",
|
||||
"noData": "No device data available"
|
||||
},
|
||||
"validation": {
|
||||
"deviceNosRequired": "Please enter device number list",
|
||||
"deviceNosMaxLength": "Cannot exceed 100 device numbers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,13 +398,15 @@
|
||||
"cardChange": "换卡网卡"
|
||||
},
|
||||
"packageManagement": {
|
||||
"title": "我的套餐",
|
||||
"title": "套餐管理",
|
||||
"packageCreate": "新建套餐",
|
||||
"packageBatch": "批量管理",
|
||||
"packageList": "我的套餐",
|
||||
"packageList": "套餐管理",
|
||||
"packageChange": "套餐变更",
|
||||
"packageAssign": "套餐分配",
|
||||
"packageAssign": "单套餐分配",
|
||||
"seriesAssign": "套餐系列分配",
|
||||
"packageSeries": "套餐系列",
|
||||
"myPackages": "代理可售套餐",
|
||||
"packageCommission": "套餐佣金网卡"
|
||||
},
|
||||
"accountManagement": {
|
||||
@@ -447,13 +449,15 @@
|
||||
"allocationRecordDetail": "分配记录详情",
|
||||
"cardReplacementRequest": "换卡申请",
|
||||
"authorizationRecords": "授权记录",
|
||||
"authorizationDetail": "授权记录详情"
|
||||
"authorizationDetail": "授权记录详情",
|
||||
"enterpriseDevices": "企业设备列表"
|
||||
},
|
||||
"account": {
|
||||
"title": "账户管理",
|
||||
"customerAccount": "客户账号",
|
||||
"myAccount": "我的账户",
|
||||
"carrierManagement": "运营商管理"
|
||||
"carrierManagement": "运营商管理",
|
||||
"orders": "订单管理"
|
||||
},
|
||||
"commission": {
|
||||
"menu": {
|
||||
@@ -751,5 +755,206 @@
|
||||
"pendingAmount": "待审核金额",
|
||||
"todayCommission": "今日佣金"
|
||||
}
|
||||
},
|
||||
"seriesBinding": {
|
||||
"buttons": {
|
||||
"batchSetSeries": "批量设置套餐系列",
|
||||
"clearBinding": "清除绑定"
|
||||
},
|
||||
"dialog": {
|
||||
"title": "批量设置套餐系列绑定",
|
||||
"titleCard": "批量设置卡的套餐系列绑定",
|
||||
"titleDevice": "批量设置设备的套餐系列绑定"
|
||||
},
|
||||
"form": {
|
||||
"seriesAllocation": "套餐系列分配",
|
||||
"seriesAllocationPlaceholder": "请选择套餐系列分配",
|
||||
"clearBindingOption": "清除关联",
|
||||
"selectedCount": "已选择 {count} 项"
|
||||
},
|
||||
"messages": {
|
||||
"noSelection": "请先选择要设置的项",
|
||||
"setSuccess": "套餐系列绑定设置成功",
|
||||
"setFailed": "套餐系列绑定设置失败",
|
||||
"partialSuccess": "部分设置成功:成功 {success} 项,失败 {fail} 项",
|
||||
"confirmSet": "确定要为选中的 {count} 项设置套餐系列绑定吗?",
|
||||
"confirmClear": "确定要清除选中的 {count} 项的套餐系列绑定吗?"
|
||||
},
|
||||
"result": {
|
||||
"title": "设置结果",
|
||||
"successCount": "成功数量",
|
||||
"failCount": "失败数量",
|
||||
"failedItems": "失败项详情",
|
||||
"iccid": "ICCID",
|
||||
"deviceId": "设备ID",
|
||||
"deviceNo": "设备号",
|
||||
"reason": "失败原因"
|
||||
}
|
||||
},
|
||||
"orderManagement": {
|
||||
"title": "订单管理",
|
||||
"orderList": "订单列表",
|
||||
"orderDetail": "订单详情",
|
||||
"orderItems": "订单明细",
|
||||
"createOrder": "创建订单",
|
||||
"searchForm": {
|
||||
"orderNo": "订单号",
|
||||
"orderNoPlaceholder": "请输入订单号",
|
||||
"paymentStatus": "支付状态",
|
||||
"paymentStatusPlaceholder": "请选择支付状态",
|
||||
"orderType": "订单类型",
|
||||
"orderTypePlaceholder": "请选择订单类型",
|
||||
"dateRange": "创建时间",
|
||||
"startDate": "开始时间",
|
||||
"endDate": "结束时间"
|
||||
},
|
||||
"table": {
|
||||
"id": "订单ID",
|
||||
"orderNo": "订单号",
|
||||
"orderType": "订单类型",
|
||||
"buyerId": "买家ID",
|
||||
"buyerType": "买家类型",
|
||||
"paymentStatus": "支付状态",
|
||||
"paymentMethod": "支付方式",
|
||||
"totalAmount": "订单金额",
|
||||
"paidAt": "支付时间",
|
||||
"commissionStatus": "佣金状态",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"operation": "操作"
|
||||
},
|
||||
"orderType": {
|
||||
"singleCard": "单卡购买",
|
||||
"device": "设备购买"
|
||||
},
|
||||
"buyerType": {
|
||||
"personal": "个人客户",
|
||||
"agent": "代理商"
|
||||
},
|
||||
"paymentStatus": {
|
||||
"pending": "待支付",
|
||||
"paid": "已支付",
|
||||
"cancelled": "已取消",
|
||||
"refunded": "已退款"
|
||||
},
|
||||
"paymentMethod": {
|
||||
"wallet": "钱包支付",
|
||||
"wechat": "微信支付",
|
||||
"alipay": "支付宝支付"
|
||||
},
|
||||
"commissionStatus": {
|
||||
"notApplicable": "不适用",
|
||||
"pending": "待结算",
|
||||
"settled": "已结算"
|
||||
},
|
||||
"actions": {
|
||||
"viewDetail": "查看详情",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"submit": "提交",
|
||||
"close": "关闭"
|
||||
},
|
||||
"createForm": {
|
||||
"packageIds": "选择套餐",
|
||||
"packageIdsPlaceholder": "请选择套餐",
|
||||
"iotCardId": "IoT卡ID",
|
||||
"iotCardIdPlaceholder": "请输入IoT卡ID",
|
||||
"deviceId": "设备ID",
|
||||
"deviceIdPlaceholder": "请输入设备ID"
|
||||
},
|
||||
"items": {
|
||||
"packageName": "套餐名称",
|
||||
"quantity": "数量",
|
||||
"unitPrice": "单价",
|
||||
"amount": "小计"
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "订单创建成功",
|
||||
"createFailed": "订单创建失败",
|
||||
"cancelSuccess": "订单取消成功",
|
||||
"cancelFailed": "订单取消失败",
|
||||
"cancelConfirm": "取消订单确认",
|
||||
"cancelConfirmText": "确定要取消该订单吗?取消后不可恢复",
|
||||
"cannotCancelPaid": "已支付的订单无法取消",
|
||||
"cannotCancelCancelled": "订单已取消",
|
||||
"cannotCancelRefunded": "已退款的订单无法取消",
|
||||
"loadFailed": "加载订单数据失败",
|
||||
"noData": "暂无订单数据"
|
||||
},
|
||||
"validation": {
|
||||
"orderTypeRequired": "请选择订单类型",
|
||||
"packageIdsRequired": "请至少选择一个套餐",
|
||||
"iotCardRequired": "请选择IoT卡",
|
||||
"deviceRequired": "请选择设备",
|
||||
"packagesMaxLimit": "最多只能选择10个套餐"
|
||||
}
|
||||
},
|
||||
"enterpriseDevices": {
|
||||
"title": "企业设备列表",
|
||||
"searchForm": {
|
||||
"enterpriseId": "企业ID",
|
||||
"enterpriseIdPlaceholder": "请输入企业ID",
|
||||
"deviceNo": "设备号",
|
||||
"deviceNoPlaceholder": "请输入设备号"
|
||||
},
|
||||
"table": {
|
||||
"deviceId": "设备ID",
|
||||
"deviceNo": "设备号",
|
||||
"deviceName": "设备名称",
|
||||
"deviceModel": "设备型号",
|
||||
"cardCount": "绑定卡数量",
|
||||
"authorizedAt": "授权时间",
|
||||
"operation": "操作"
|
||||
},
|
||||
"buttons": {
|
||||
"allocateDevices": "授权设备",
|
||||
"recallDevices": "撤销授权",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"dialog": {
|
||||
"allocateTitle": "授权设备给企业",
|
||||
"recallTitle": "撤销设备授权",
|
||||
"resultTitle": "操作结果"
|
||||
},
|
||||
"form": {
|
||||
"deviceNos": "设备号列表",
|
||||
"deviceNosPlaceholder": "请输入设备号,支持换行或逗号分隔",
|
||||
"deviceNosHint": "每行一个设备号或使用逗号分隔,最多100个",
|
||||
"remark": "备注",
|
||||
"remarkPlaceholder": "请输入授权备注(可选)",
|
||||
"selectedDevices": "已选择设备",
|
||||
"selectedCount": "已选择 {count} 个设备"
|
||||
},
|
||||
"result": {
|
||||
"successCount": "成功数量",
|
||||
"failCount": "失败数量",
|
||||
"authorizedDevices": "已授权设备",
|
||||
"failedItems": "失败项",
|
||||
"deviceNo": "设备号",
|
||||
"deviceId": "设备ID",
|
||||
"cardCount": "绑定卡数",
|
||||
"reason": "失败原因"
|
||||
},
|
||||
"messages": {
|
||||
"allocateSuccess": "设备授权成功",
|
||||
"allocateFailed": "设备授权失败",
|
||||
"allocatePartialSuccess": "部分设备授权成功:成功 {success} 个,失败 {fail} 个",
|
||||
"recallSuccess": "撤销授权成功",
|
||||
"recallFailed": "撤销授权失败",
|
||||
"recallPartialSuccess": "部分设备撤销成功:成功 {success} 个,失败 {fail} 个",
|
||||
"recallConfirm": "撤销授权确认",
|
||||
"recallConfirmText": "确定要撤销选中的 {count} 个设备的授权吗?",
|
||||
"noSelection": "请先选择要撤销的设备",
|
||||
"deviceNosRequired": "请输入设备号",
|
||||
"deviceNosEmpty": "设备号列表不能为空",
|
||||
"deviceNosMaxLimit": "设备号数量不能超过100个",
|
||||
"invalidDeviceNos": "存在无效的设备号格式",
|
||||
"loadFailed": "加载设备列表失败",
|
||||
"noData": "暂无设备数据"
|
||||
},
|
||||
"validation": {
|
||||
"deviceNosRequired": "请输入设备号列表",
|
||||
"deviceNosMaxLength": "设备号数量不能超过100个"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +694,62 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/package-management',
|
||||
name: 'PackageManagement',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.title',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'package-series',
|
||||
name: 'PackageSeries',
|
||||
component: RoutesAlias.PackageSeries,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.packageSeries',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'package-list',
|
||||
name: 'PackageList',
|
||||
component: RoutesAlias.PackageList,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.packageList',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'my-packages',
|
||||
name: 'MyPackages',
|
||||
component: RoutesAlias.MyPackages,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.myPackages',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'package-assign',
|
||||
name: 'PackageAssign',
|
||||
component: RoutesAlias.PackageAssign,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.packageAssign',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'series-assign',
|
||||
name: 'SeriesAssign',
|
||||
component: RoutesAlias.SeriesAssign,
|
||||
meta: {
|
||||
title: 'menus.packageManagement.seriesAssign',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/account-management',
|
||||
name: 'AccountManagement',
|
||||
@@ -954,6 +1010,16 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'enterprise-devices',
|
||||
name: 'EnterpriseDevices',
|
||||
component: RoutesAlias.EnterpriseDevices,
|
||||
meta: {
|
||||
title: 'menus.assetManagement.enterpriseDevices',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -993,6 +1059,15 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'OrderManagement',
|
||||
component: RoutesAlias.OrderList,
|
||||
meta: {
|
||||
title: 'menus.account.orders',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// {
|
||||
// path: 'my-account',
|
||||
// name: 'MyAccount',
|
||||
|
||||
@@ -67,10 +67,12 @@ export enum RoutesAlias {
|
||||
// 我的套餐
|
||||
PackageCreate = '/package-management/package-create', // 新建套餐
|
||||
PackageBatch = '/package-management/package-batch', // 批量管理
|
||||
PackageList = '/package-management/package-list', // 我的套餐
|
||||
PackageList = '/package-management/package-list', // 套餐管理
|
||||
PackageChange = '/package-management/package-change', // 套餐变更
|
||||
PackageAssign = '/package-management/package-assign', // 套餐分配
|
||||
PackageAssign = '/package-management/package-assign', // 单套餐分配
|
||||
SeriesAssign = '/package-management/series-assign', // 套餐系列分配
|
||||
PackageSeries = '/package-management/package-series', // 套餐系列
|
||||
MyPackages = '/package-management/my-packages', // 代理可售套餐
|
||||
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
|
||||
|
||||
// 账号管理
|
||||
@@ -102,11 +104,13 @@ export enum RoutesAlias {
|
||||
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
|
||||
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
|
||||
AuthorizationDetail = '/asset-management/authorization-detail', // 授权记录详情
|
||||
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备列表
|
||||
|
||||
// 账户管理
|
||||
CustomerAccountList = '/finance/customer-account', // 客户账号
|
||||
MyAccount = '/finance/my-account', // 我的账户
|
||||
CarrierManagement = '/finance/carrier-management', // 运营商管理
|
||||
OrderList = '/order-management/order-list', // 订单管理
|
||||
|
||||
// 佣金管理
|
||||
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
|
||||
|
||||
@@ -478,3 +478,24 @@ export interface IotCardDetailResponse {
|
||||
created_at: string // 创建时间
|
||||
updated_at: string // 更新时间
|
||||
}
|
||||
|
||||
// ========== 批量设置卡的套餐系列绑定相关 ==========
|
||||
|
||||
// 批量设置卡的套餐系列绑定请求参数
|
||||
export interface BatchSetCardSeriesBindingRequest {
|
||||
iccids: string[] // ICCID列表(最多500个)
|
||||
series_allocation_id: number // 套餐系列分配ID(0表示清除关联)
|
||||
}
|
||||
|
||||
// 卡套餐系列绑定失败项
|
||||
export interface CardSeriesBindingFailedItem {
|
||||
iccid: string // ICCID
|
||||
reason: string // 失败原因
|
||||
}
|
||||
|
||||
// 批量设置卡的套餐系列绑定响应
|
||||
export interface BatchSetCardSeriesBindingResponse {
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表
|
||||
}
|
||||
|
||||
@@ -119,7 +119,6 @@ export interface CreateWithdrawalSettingParams {
|
||||
min_withdrawal_amount: number // 最低提现金额(分)
|
||||
fee_rate: number // 手续费比率(基点,100=1%)
|
||||
daily_withdrawal_limit: number // 每日提现次数限制
|
||||
arrival_days: number // 到账天数
|
||||
}
|
||||
|
||||
// ==================== 佣金记录相关 ====================
|
||||
@@ -248,3 +247,50 @@ export interface ShopCommissionSummaryPageResult {
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// ==================== 我的佣金统计相关 ====================
|
||||
|
||||
/**
|
||||
* 我的佣金统计查询参数
|
||||
*/
|
||||
export interface MyCommissionStatsQueryParams {
|
||||
shop_id?: number // 店铺ID
|
||||
start_time?: string // 开始时间
|
||||
end_time?: string // 结束时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的佣金统计响应
|
||||
*/
|
||||
export interface MyCommissionStatsResponse {
|
||||
cost_diff_amount: number // 成本价差收入(分)
|
||||
cost_diff_count: number // 成本价差笔数
|
||||
cost_diff_percent: number // 成本价差占比(千分比)
|
||||
one_time_amount: number // 一次性佣金收入(分)
|
||||
one_time_count: number // 一次性佣金笔数
|
||||
one_time_percent: number // 一次性佣金占比(千分比)
|
||||
tier_bonus_amount: number // 梯度奖励收入(分)
|
||||
tier_bonus_count: number // 梯度奖励笔数
|
||||
tier_bonus_percent: number // 梯度奖励占比(千分比)
|
||||
total_amount: number // 总收入(分)
|
||||
total_count: number // 总笔数
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的每日佣金统计查询参数
|
||||
*/
|
||||
export interface MyDailyCommissionStatsQueryParams {
|
||||
shop_id?: number // 店铺ID
|
||||
start_date?: string // 开始日期(YYYY-MM-DD)
|
||||
end_date?: string // 结束日期(YYYY-MM-DD)
|
||||
days?: number // 查询天数(默认30天,最大365天)
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日佣金统计项
|
||||
*/
|
||||
export interface DailyCommissionStatsItem {
|
||||
date: string // 日期(YYYY-MM-DD)
|
||||
total_amount: number // 当日总收入(分)
|
||||
total_count: number // 当日总笔数
|
||||
}
|
||||
|
||||
@@ -29,11 +29,25 @@ export interface PaginationData<T> {
|
||||
pages?: number
|
||||
}
|
||||
|
||||
// 新版分页响应数据(使用 list 字段)
|
||||
export interface PaginationDataV2<T> {
|
||||
list: T[] // 新版API使用 list 字段
|
||||
total: number
|
||||
page_size: number
|
||||
page: number
|
||||
total_pages: number // 总页数
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface PaginationResponse<T = any> extends BaseResponse {
|
||||
data: PaginationData<T>
|
||||
}
|
||||
|
||||
// 新版分页响应(使用 list 字段)
|
||||
export interface PaginationResponseV2<T = any> extends BaseResponse {
|
||||
data: PaginationDataV2<T>
|
||||
}
|
||||
|
||||
// 列表响应
|
||||
export interface ListResponse<T = any> extends BaseResponse {
|
||||
data: T[]
|
||||
|
||||
@@ -202,3 +202,25 @@ export interface DeviceImportTaskDetail extends DeviceImportTask {
|
||||
failed_items: DeviceImportResultItem[] | null // 失败记录详情
|
||||
skipped_items: DeviceImportResultItem[] | null // 跳过记录详情
|
||||
}
|
||||
|
||||
// ========== 批量设置设备的套餐系列绑定相关 ==========
|
||||
|
||||
// 批量设置设备的套餐系列绑定请求参数
|
||||
export interface BatchSetDeviceSeriesBindingRequest {
|
||||
device_ids: number[] // 设备ID列表(最多500个)
|
||||
series_allocation_id: number // 套餐系列分配ID(0表示清除关联)
|
||||
}
|
||||
|
||||
// 设备套餐系列绑定失败项
|
||||
export interface DeviceSeriesBindingFailedItem {
|
||||
device_id: number // 设备ID
|
||||
device_no: string // 设备号
|
||||
reason: string // 失败原因
|
||||
}
|
||||
|
||||
// 批量设置设备的套餐系列绑定响应
|
||||
export interface BatchSetDeviceSeriesBindingResponse {
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表
|
||||
}
|
||||
|
||||
89
src/types/api/enterpriseDevice.ts
Normal file
89
src/types/api/enterpriseDevice.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 企业设备授权相关类型定义
|
||||
*/
|
||||
|
||||
import { PaginationParams } from '@/types'
|
||||
|
||||
// ==================== 企业设备列表相关 ====================
|
||||
|
||||
/**
|
||||
* 企业设备列表项
|
||||
*/
|
||||
export interface EnterpriseDeviceItem {
|
||||
device_id: number // 设备ID
|
||||
device_no: string // 设备号
|
||||
device_name: string // 设备名称
|
||||
device_model: string // 设备型号
|
||||
card_count: number // 绑定卡数量
|
||||
authorized_at: string // 授权时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业设备列表查询参数
|
||||
*/
|
||||
export interface EnterpriseDeviceListParams extends PaginationParams {
|
||||
device_no?: string // 设备号(模糊搜索)
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业设备列表分页结果
|
||||
*/
|
||||
export interface EnterpriseDevicePageResult {
|
||||
list: EnterpriseDeviceItem[] | null // 设备列表
|
||||
total: number // 总数
|
||||
}
|
||||
|
||||
// ==================== 授权设备相关 ====================
|
||||
|
||||
/**
|
||||
* 授权设备请求参数
|
||||
*/
|
||||
export interface AllocateDevicesRequest {
|
||||
device_nos: string[] | null // 设备号列表(最多100个)
|
||||
remark?: string // 授权备注
|
||||
}
|
||||
|
||||
/**
|
||||
* 已授权设备项
|
||||
*/
|
||||
export interface AuthorizedDeviceItem {
|
||||
device_id: number // 设备ID
|
||||
device_no: string // 设备号
|
||||
card_count: number // 绑定卡数量
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败设备项
|
||||
*/
|
||||
export interface FailedDeviceItem {
|
||||
device_no: string // 设备号
|
||||
reason: string // 失败原因
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权设备响应
|
||||
*/
|
||||
export interface AllocateDevicesResponse {
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
authorized_devices: AuthorizedDeviceItem[] | null // 已授权设备列表
|
||||
failed_items: FailedDeviceItem[] | null // 失败项列表
|
||||
}
|
||||
|
||||
// ==================== 撤销授权相关 ====================
|
||||
|
||||
/**
|
||||
* 撤销设备授权请求参数
|
||||
*/
|
||||
export interface RecallDevicesRequest {
|
||||
device_nos: string[] | null // 设备号列表(最多100个)
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销设备授权响应
|
||||
*/
|
||||
export interface RecallDevicesResponse {
|
||||
success_count: number // 成功数量
|
||||
fail_count: number // 失败数量
|
||||
failed_items: FailedDeviceItem[] | null // 失败项列表
|
||||
}
|
||||
@@ -29,8 +29,23 @@ export * from './shop'
|
||||
// 网卡相关
|
||||
export * from './card'
|
||||
|
||||
// 套餐相关
|
||||
export * from './package'
|
||||
// 套餐相关(旧)- 为避免冲突,保留旧类型但使用别名
|
||||
export type {
|
||||
PackageStatus,
|
||||
PackageType as OldPackageType,
|
||||
PackageSeries as OldPackageSeries,
|
||||
Package,
|
||||
PackageQueryParams as OldPackageQueryParams,
|
||||
PackageSeriesQueryParams as OldPackageSeriesQueryParams,
|
||||
PackageSeriesFormData,
|
||||
PackageFormData,
|
||||
PackageAssignParams,
|
||||
PackageChangeRecord,
|
||||
PackageAssignRecord
|
||||
} from './package'
|
||||
|
||||
// 套餐管理系统相关(新)
|
||||
export * from './packageManagement'
|
||||
|
||||
// 设备相关
|
||||
export * from './device'
|
||||
@@ -53,5 +68,11 @@ export * from './authorization'
|
||||
// 企业卡授权相关
|
||||
export * from './enterpriseCard'
|
||||
|
||||
// 企业设备授权相关
|
||||
export * from './enterpriseDevice'
|
||||
|
||||
// 运营商相关
|
||||
export * from './carrier'
|
||||
|
||||
// 订单相关
|
||||
export * from './order'
|
||||
|
||||
90
src/types/api/order.ts
Normal file
90
src/types/api/order.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 订单管理相关类型定义
|
||||
*/
|
||||
|
||||
// 支付状态枚举
|
||||
export enum PaymentStatus {
|
||||
PENDING = 1, // 待支付
|
||||
PAID = 2, // 已支付
|
||||
CANCELLED = 3, // 已取消
|
||||
REFUNDED = 4 // 已退款
|
||||
}
|
||||
|
||||
// 订单类型
|
||||
export type OrderType = 'single_card' | 'device'
|
||||
|
||||
// 买家类型
|
||||
export type BuyerType = 'personal' | 'agent'
|
||||
|
||||
// 订单支付方式
|
||||
export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay'
|
||||
|
||||
// 订单佣金状态
|
||||
export enum OrderCommissionStatus {
|
||||
NOT_APPLICABLE = 0, // 不适用
|
||||
PENDING = 1, // 待结算
|
||||
SETTLED = 2 // 已结算
|
||||
}
|
||||
|
||||
// 订单明细
|
||||
export interface OrderItem {
|
||||
id: number
|
||||
package_id: number
|
||||
package_name: string
|
||||
quantity: number
|
||||
unit_price: number // 单价(分)
|
||||
amount: number // 小计金额(分)
|
||||
}
|
||||
|
||||
// 订单响应
|
||||
export interface Order {
|
||||
id: number
|
||||
order_no: string
|
||||
order_type: OrderType
|
||||
buyer_id: number
|
||||
buyer_type: BuyerType
|
||||
payment_status: PaymentStatus
|
||||
payment_status_text: string
|
||||
payment_method: OrderPaymentMethod
|
||||
total_amount: number // 订单总金额(分)
|
||||
paid_at: string | null
|
||||
commission_status: OrderCommissionStatus
|
||||
commission_config_version: number
|
||||
device_id: number | null
|
||||
iot_card_id: number | null
|
||||
items: OrderItem[] | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 订单查询参数
|
||||
export interface OrderQueryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
payment_status?: PaymentStatus
|
||||
order_type?: OrderType
|
||||
order_no?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
dateRange?: string[] | any // For date range picker in UI
|
||||
}
|
||||
|
||||
// 订单列表响应
|
||||
export interface OrderListResponse {
|
||||
items: Order[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
total_pages: number
|
||||
}
|
||||
|
||||
// 创建订单请求
|
||||
export interface CreateOrderRequest {
|
||||
order_type: OrderType
|
||||
package_ids: number[]
|
||||
iot_card_id?: number | null
|
||||
device_id?: number | null
|
||||
}
|
||||
|
||||
// 创建订单响应 (返回订单详情)
|
||||
export type CreateOrderResponse = Order
|
||||
332
src/types/api/packageManagement.ts
Normal file
332
src/types/api/packageManagement.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 套餐管理系统类型定义
|
||||
* 使用下划线命名与后端 API 保持一致
|
||||
*/
|
||||
|
||||
import { PaginationParams } from './common'
|
||||
|
||||
// ==================== 套餐系列管理 ====================
|
||||
|
||||
/**
|
||||
* 套餐系列响应
|
||||
*/
|
||||
export interface PackageSeriesResponse {
|
||||
id: number
|
||||
series_code: string
|
||||
series_name: string
|
||||
description?: string
|
||||
status: number // 1:启用, 2:禁用
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐系列查询参数
|
||||
*/
|
||||
export interface PackageSeriesQueryParams extends PaginationParams {
|
||||
series_name?: string // 系列名称(模糊搜索)
|
||||
status?: number // 状态筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐系列请求
|
||||
*/
|
||||
export interface CreatePackageSeriesRequest {
|
||||
series_code: string // 系列编码,必填
|
||||
series_name: string // 系列名称,必填
|
||||
description?: string // 描述,可选
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列请求
|
||||
*/
|
||||
export interface UpdatePackageSeriesRequest {
|
||||
series_code?: string
|
||||
series_name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列状态请求
|
||||
*/
|
||||
export interface UpdatePackageSeriesStatusRequest {
|
||||
status: number // 1:启用, 2:禁用
|
||||
}
|
||||
|
||||
// ==================== 套餐管理 ====================
|
||||
|
||||
/**
|
||||
* 套餐响应
|
||||
*/
|
||||
export interface PackageResponse {
|
||||
id: number
|
||||
package_code: string
|
||||
package_name: string
|
||||
series_id: number
|
||||
series_name?: string
|
||||
package_type: string // 'formal':正式套餐, 'addon':加油包
|
||||
data_type: string // 'real':真实流量, 'virtual':虚拟流量
|
||||
real_data_mb: number // 真流量额度(MB)
|
||||
virtual_data_mb: number // 虚流量额度(MB)
|
||||
duration_months: number // 有效期(月)
|
||||
price: number // 价格(分)
|
||||
shelf_status: number // 上架状态 (1:上架, 2:下架)
|
||||
status: number // 状态 (1:启用, 2:禁用)
|
||||
description?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐查询参数
|
||||
*/
|
||||
export interface PackageQueryParams extends PaginationParams {
|
||||
package_name?: string // 套餐名称(模糊搜索)
|
||||
series_id?: number // 系列ID
|
||||
package_type?: string // 套餐类型
|
||||
data_type?: string // 流量类型
|
||||
shelf_status?: number // 上架状态
|
||||
status?: number // 状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐请求
|
||||
*/
|
||||
export interface CreatePackageRequest {
|
||||
package_code: string // 套餐编码,必填
|
||||
package_name: string // 套餐名称,必填
|
||||
series_id: number // 所属系列ID,必填
|
||||
package_type: string // 套餐类型,必填
|
||||
data_type: string // 流量类型,必填
|
||||
real_data_mb: number // 真流量额度(MB),必填
|
||||
virtual_data_mb: number // 虚流量额度(MB),必填
|
||||
duration_months: number // 有效期(月),必填
|
||||
price: number // 价格(分),必填
|
||||
description?: string // 描述,可选
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐请求
|
||||
*/
|
||||
export interface UpdatePackageRequest {
|
||||
package_code?: string
|
||||
package_name?: string
|
||||
series_id?: number
|
||||
package_type?: string
|
||||
data_type?: string
|
||||
real_data_mb?: number
|
||||
virtual_data_mb?: number
|
||||
duration_months?: number
|
||||
price?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐状态请求
|
||||
*/
|
||||
export interface UpdatePackageStatusRequest {
|
||||
status: number // 1:启用, 2:禁用
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐上架状态请求
|
||||
*/
|
||||
export interface UpdatePackageShelfStatusRequest {
|
||||
shelf_status: number // 1:上架, 2:下架
|
||||
}
|
||||
|
||||
/**
|
||||
* 系列下拉选项响应
|
||||
*/
|
||||
export interface SeriesSelectOption {
|
||||
id: number
|
||||
series_name: string
|
||||
series_code: string
|
||||
}
|
||||
|
||||
// ==================== 代理可售套餐 ====================
|
||||
|
||||
/**
|
||||
* 我的可售套餐响应
|
||||
*/
|
||||
export interface MyPackageResponse {
|
||||
id: number
|
||||
package_code: string
|
||||
package_name: string
|
||||
series_id: number
|
||||
series_name: string
|
||||
package_type: string
|
||||
data_type: string
|
||||
real_data_mb: number // 真流量额度(MB)
|
||||
virtual_data_mb: number // 虚流量额度(MB)
|
||||
duration_months: number
|
||||
price: number // 套餐原价(分)
|
||||
cost_price: number // 成本价(分)
|
||||
suggested_retail_price: number // 建议售价(分)
|
||||
profit_margin: number // 利润空间(分)
|
||||
price_source: string // 价格来源:'series_pricing':系列加价, 'package_override':单套餐覆盖
|
||||
shelf_status: number
|
||||
status: number
|
||||
description?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的可售套餐查询参数
|
||||
*/
|
||||
export interface MyPackageQueryParams extends PaginationParams {
|
||||
series_id?: number // 系列ID筛选
|
||||
package_type?: string // 套餐类型筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的被分配系列响应
|
||||
*/
|
||||
export interface MySeriesAllocationResponse {
|
||||
id: number // 分配ID
|
||||
series_id: number
|
||||
series_code: string
|
||||
series_name: string
|
||||
pricing_mode: string // 定价模式:'fixed':固定金额, 'percent':百分比
|
||||
pricing_value: number // 定价值
|
||||
allocator_shop_name: string // 分配者店铺名称
|
||||
package_count: number // 可售套餐数量
|
||||
status: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// ==================== 单套餐分配 ====================
|
||||
|
||||
/**
|
||||
* 单套餐分配响应
|
||||
*/
|
||||
export interface ShopPackageAllocationResponse {
|
||||
id: number
|
||||
package_id: number
|
||||
package_code: string
|
||||
package_name: string
|
||||
shop_id: number
|
||||
shop_name: string
|
||||
cost_price: number // 覆盖的成本价(分)
|
||||
calculated_cost_price: number // 原计算成本价(分,供参考)
|
||||
allocation_id?: number // 关联的系列分配ID
|
||||
status: number // 1:启用, 2:禁用
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 单套餐分配查询参数
|
||||
*/
|
||||
export interface ShopPackageAllocationQueryParams extends PaginationParams {
|
||||
shop_id?: number // 店铺ID筛选
|
||||
package_id?: number // 套餐ID筛选
|
||||
status?: number // 状态筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单套餐分配请求
|
||||
*/
|
||||
export interface CreateShopPackageAllocationRequest {
|
||||
package_id: number // 套餐ID,必填
|
||||
shop_id: number // 店铺ID,必填
|
||||
cost_price: number // 覆盖的成本价(分),必填
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单套餐分配请求
|
||||
*/
|
||||
export interface UpdateShopPackageAllocationRequest {
|
||||
cost_price: number // 只允许修改成本价
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单套餐分配状态请求
|
||||
*/
|
||||
export interface UpdateShopPackageAllocationStatusRequest {
|
||||
status: number // 1:启用, 2:禁用
|
||||
}
|
||||
|
||||
// ==================== 套餐系列分配 ====================
|
||||
|
||||
/**
|
||||
* 基础返佣配置
|
||||
*/
|
||||
export interface BaseCommissionConfig {
|
||||
mode: 'fixed' | 'percent' // 返佣模式:'fixed':固定金额, 'percent':百分比
|
||||
value: number // 返佣值:固定金额(分)或百分比的千分比(如200表示20%)
|
||||
}
|
||||
|
||||
/**
|
||||
* 梯度档位配置
|
||||
*/
|
||||
export interface TierEntry {
|
||||
threshold: number // 阈值(销量或销售额)
|
||||
mode: 'fixed' | 'percent' // 返佣模式
|
||||
value: number // 返佣值
|
||||
}
|
||||
|
||||
/**
|
||||
* 梯度返佣配置
|
||||
*/
|
||||
export interface TierCommissionConfig {
|
||||
period_type: 'monthly' | 'quarterly' | 'yearly' // 周期类型
|
||||
tier_type: 'sales_count' | 'sales_amount' // 梯度类型:销量或销售额
|
||||
tiers: TierEntry[] // 梯度档位数组
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐系列分配响应
|
||||
*/
|
||||
export interface ShopSeriesAllocationResponse {
|
||||
id: number
|
||||
series_id: number
|
||||
series_name: string
|
||||
shop_id: number
|
||||
shop_name: string
|
||||
allocator_shop_id: number // 分配者店铺ID
|
||||
allocator_shop_name: string // 分配者店铺名称
|
||||
base_commission: BaseCommissionConfig // 基础返佣配置
|
||||
enable_tier_commission: boolean // 是否启用梯度返佣
|
||||
tier_config?: TierCommissionConfig // 梯度返佣配置(可选)
|
||||
status: number // 1:启用, 2:禁用
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐系列分配查询参数
|
||||
*/
|
||||
export interface ShopSeriesAllocationQueryParams extends PaginationParams {
|
||||
shop_id?: number // 店铺ID筛选
|
||||
series_id?: number // 系列ID筛选
|
||||
status?: number // 状态筛选
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐系列分配请求
|
||||
*/
|
||||
export interface CreateShopSeriesAllocationRequest {
|
||||
series_id: number // 套餐系列ID,必填
|
||||
shop_id: number // 店铺ID,必填
|
||||
base_commission: BaseCommissionConfig // 基础返佣配置,必填
|
||||
enable_tier_commission?: boolean // 是否启用梯度返佣,可选(默认false)
|
||||
tier_config?: TierCommissionConfig // 梯度返佣配置,当enable_tier_commission为true时必填
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列分配请求
|
||||
*/
|
||||
export interface UpdateShopSeriesAllocationRequest {
|
||||
base_commission?: BaseCommissionConfig // 基础返佣配置
|
||||
enable_tier_commission?: boolean // 是否启用梯度返佣
|
||||
tier_config?: TierCommissionConfig // 梯度返佣配置
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐系列分配状态请求
|
||||
*/
|
||||
export interface UpdateShopSeriesAllocationStatusRequest {
|
||||
status: number // 1:启用, 2:禁用
|
||||
}
|
||||
@@ -222,10 +222,11 @@ function handleErrorMessage(error: any, mode: ErrorMessageMode = 'message') {
|
||||
async function request<T = any>(config: ExtendedRequestConfig): Promise<T> {
|
||||
const processedConfig = processRequestConfig(config)
|
||||
|
||||
// 对 POST | PUT 请求特殊处理
|
||||
// 对 POST | PUT | PATCH 请求特殊处理
|
||||
if (
|
||||
processedConfig.method?.toUpperCase() === 'POST' ||
|
||||
processedConfig.method?.toUpperCase() === 'PUT'
|
||||
processedConfig.method?.toUpperCase() === 'PUT' ||
|
||||
processedConfig.method?.toUpperCase() === 'PATCH'
|
||||
) {
|
||||
// 如果已经有 data,则保留原有的 data
|
||||
if (processedConfig.params && !processedConfig.data) {
|
||||
@@ -258,6 +259,9 @@ const api = {
|
||||
put<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'PUT' }) // PUT 请求
|
||||
},
|
||||
patch<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'PATCH' }) // PATCH 请求
|
||||
},
|
||||
del<T>(config: ExtendedRequestConfig): Promise<T> {
|
||||
return request({ ...config, method: 'DELETE' }) // DELETE 请求
|
||||
},
|
||||
|
||||
@@ -427,7 +427,7 @@
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入账号名称', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
|
||||
@@ -536,7 +536,7 @@
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入账号名称', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<ElButton type="warning" :disabled="selectedCards.length === 0" @click="showRecallDialog">
|
||||
批量回收
|
||||
</ElButton>
|
||||
<ElButton type="info" :disabled="selectedCards.length === 0" @click="showSeriesBindingDialog">
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -259,6 +262,75 @@
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 批量设置套餐系列绑定对话框 -->
|
||||
<ElDialog
|
||||
v-model="seriesBindingDialogVisible"
|
||||
title="批量设置套餐系列绑定"
|
||||
width="600px"
|
||||
@close="handleSeriesBindingDialogClose"
|
||||
>
|
||||
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
|
||||
<ElFormItem label="已选择卡数">
|
||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
|
||||
<ElSelect
|
||||
v-model="seriesBindingForm.series_allocation_id"
|
||||
placeholder="请选择套餐系列分配(选择清除关联将解除绑定)"
|
||||
style="width: 100%"
|
||||
:loading="seriesLoading"
|
||||
>
|
||||
<ElOption label="清除关联" :value="0" />
|
||||
<ElOption
|
||||
v-for="series in seriesAllocationList"
|
||||
:key="series.id"
|
||||
:label="`${series.series_name} (${series.shop_name})`"
|
||||
:value="series.id"
|
||||
:disabled="series.status !== 1"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="seriesBindingDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSeriesBinding" :loading="seriesBindingLoading">
|
||||
确认设置
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 套餐系列绑定结果对话框 -->
|
||||
<ElDialog
|
||||
v-model="seriesBindingResultDialogVisible"
|
||||
title="设置结果"
|
||||
width="700px"
|
||||
>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<ElTag type="danger">{{ seriesBindingResult.fail_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0" style="margin-top: 20px">
|
||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||
<ElTable :data="seriesBindingResult.failed_items" border max-height="300">
|
||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
||||
<ElTableColumn prop="reason" label="失败原因" />
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false">确定</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -267,6 +339,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { CardService, StorageService } from '@/api/modules'
|
||||
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
||||
import { ElMessage, ElTag, ElUpload } from 'element-plus'
|
||||
import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
@@ -277,8 +350,10 @@
|
||||
StandaloneCardStatus,
|
||||
AllocateStandaloneCardsRequest,
|
||||
RecallStandaloneCardsRequest,
|
||||
AllocateStandaloneCardsResponse
|
||||
AllocateStandaloneCardsResponse,
|
||||
BatchSetCardSeriesBindingResponse
|
||||
} from '@/types/api/card'
|
||||
import type { ShopSeriesAllocationResponse } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'StandaloneCardList' })
|
||||
|
||||
@@ -306,6 +381,25 @@
|
||||
failed_items: null
|
||||
})
|
||||
|
||||
// 套餐系列绑定相关
|
||||
const seriesBindingDialogVisible = ref(false)
|
||||
const seriesBindingLoading = ref(false)
|
||||
const seriesBindingResultDialogVisible = ref(false)
|
||||
const seriesBindingFormRef = ref<FormInstance>()
|
||||
const seriesLoading = ref(false)
|
||||
const seriesAllocationList = ref<ShopSeriesAllocationResponse[]>([])
|
||||
const seriesBindingForm = reactive({
|
||||
series_allocation_id: undefined as number | undefined
|
||||
})
|
||||
const seriesBindingRules = reactive<FormRules>({
|
||||
series_allocation_id: [{ required: true, message: '请选择套餐系列分配', trigger: 'change' }]
|
||||
})
|
||||
const seriesBindingResult = ref<BatchSetCardSeriesBindingResponse>({
|
||||
success_count: 0,
|
||||
fail_count: 0,
|
||||
failed_items: null
|
||||
})
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
status: undefined,
|
||||
@@ -937,6 +1031,102 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示套餐系列绑定对话框
|
||||
const showSeriesBindingDialog = async () => {
|
||||
if (selectedCards.value.length === 0) {
|
||||
ElMessage.warning('请先选择要设置的卡')
|
||||
return
|
||||
}
|
||||
|
||||
// 加载套餐系列分配列表
|
||||
await loadSeriesAllocationList()
|
||||
|
||||
seriesBindingDialogVisible.value = true
|
||||
seriesBindingForm.series_allocation_id = undefined
|
||||
if (seriesBindingFormRef.value) {
|
||||
seriesBindingFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载套餐系列分配列表
|
||||
const loadSeriesAllocationList = async () => {
|
||||
seriesLoading.value = true
|
||||
try {
|
||||
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
|
||||
page: 1,
|
||||
page_size: 1000 // 获取所有可用的套餐系列分配
|
||||
})
|
||||
if (res.code === 0 && res.data.list) {
|
||||
seriesAllocationList.value = res.data.list
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取套餐系列分配列表失败')
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭套餐系列绑定对话框
|
||||
const handleSeriesBindingDialogClose = () => {
|
||||
if (seriesBindingFormRef.value) {
|
||||
seriesBindingFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行套餐系列绑定
|
||||
const handleSeriesBinding = async () => {
|
||||
if (!seriesBindingFormRef.value) return
|
||||
|
||||
await seriesBindingFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const iccids = selectedCards.value.map((card) => card.iccid)
|
||||
|
||||
if (iccids.length === 0) {
|
||||
ElMessage.warning('请先选择要设置的卡')
|
||||
return
|
||||
}
|
||||
|
||||
seriesBindingLoading.value = true
|
||||
try {
|
||||
const res = await CardService.batchSetCardSeriesBinding({
|
||||
iccids,
|
||||
series_allocation_id: seriesBindingForm.series_allocation_id!
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
seriesBindingResult.value = res.data
|
||||
seriesBindingDialogVisible.value = false
|
||||
seriesBindingResultDialogVisible.value = true
|
||||
|
||||
// 清空选择
|
||||
if (tableRef.value) {
|
||||
tableRef.value.clearSelection()
|
||||
}
|
||||
selectedCards.value = []
|
||||
|
||||
// 刷新列表
|
||||
getTableData()
|
||||
|
||||
// 显示消息提示
|
||||
if (res.data.fail_count === 0) {
|
||||
ElMessage.success('套餐系列绑定设置成功')
|
||||
} else if (res.data.success_count === 0) {
|
||||
ElMessage.error('套餐系列绑定设置失败')
|
||||
} else {
|
||||
ElMessage.warning(`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count} 项`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('套餐系列绑定设置失败,请重试')
|
||||
} finally {
|
||||
seriesBindingLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<ElButton @click="handleBatchRecall" :disabled="!selectedDevices.length">
|
||||
批量回收
|
||||
</ElButton>
|
||||
<ElButton type="info" @click="handleBatchSetSeries" :disabled="!selectedDevices.length">
|
||||
批量设置套餐系列
|
||||
</ElButton>
|
||||
<ElButton @click="handleImportDevice">导入设备</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
@@ -177,6 +180,71 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 批量设置套餐系列绑定对话框 -->
|
||||
<ElDialog v-model="seriesBindingDialogVisible" title="批量设置设备套餐系列绑定" width="600px">
|
||||
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
|
||||
<ElFormItem label="已选设备数">
|
||||
<span style="color: #409eff; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
||||
</ElFormItem>
|
||||
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
|
||||
<ElSelect
|
||||
v-model="seriesBindingForm.series_allocation_id"
|
||||
placeholder="请选择套餐系列分配(选择清除关联将解除绑定)"
|
||||
style="width: 100%"
|
||||
:loading="seriesLoading"
|
||||
>
|
||||
<ElOption label="清除关联" :value="0" />
|
||||
<ElOption
|
||||
v-for="series in seriesAllocationList"
|
||||
:key="series.id"
|
||||
:label="`${series.series_name} (${series.shop_name})`"
|
||||
:value="series.id"
|
||||
:disabled="series.status !== 1"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<!-- 设置结果 -->
|
||||
<div v-if="seriesBindingResult" style="margin-top: 20px">
|
||||
<ElAlert
|
||||
:type="seriesBindingResult.fail_count === 0 ? 'success' : 'warning'"
|
||||
:closable="false"
|
||||
style="margin-bottom: 10px"
|
||||
>
|
||||
<template #title>
|
||||
成功设置 {{ seriesBindingResult.success_count }} 台,失败 {{ seriesBindingResult.fail_count }} 台
|
||||
</template>
|
||||
</ElAlert>
|
||||
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0">
|
||||
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
||||
<div
|
||||
v-for="item in seriesBindingResult.failed_items"
|
||||
:key="item.device_id"
|
||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
||||
>
|
||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="handleCloseSeriesBindingDialog">
|
||||
{{ seriesBindingResult ? '关闭' : '取消' }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="!seriesBindingResult"
|
||||
type="primary"
|
||||
@click="handleConfirmSeriesBinding"
|
||||
:loading="seriesBindingLoading"
|
||||
>
|
||||
确认设置
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -186,19 +254,22 @@
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DeviceService, ShopService } from '@/api/modules'
|
||||
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
Device,
|
||||
DeviceStatus,
|
||||
AllocateDevicesResponse,
|
||||
RecallDevicesResponse
|
||||
RecallDevicesResponse,
|
||||
BatchSetDeviceSeriesBindingResponse
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
import type { ShopSeriesAllocationResponse } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'DeviceList' })
|
||||
|
||||
@@ -216,6 +287,20 @@
|
||||
const allocateResult = ref<AllocateDevicesResponse | null>(null)
|
||||
const recallResult = ref<RecallDevicesResponse | null>(null)
|
||||
|
||||
// 套餐系列绑定相关
|
||||
const seriesBindingDialogVisible = ref(false)
|
||||
const seriesBindingLoading = ref(false)
|
||||
const seriesBindingFormRef = ref<FormInstance>()
|
||||
const seriesLoading = ref(false)
|
||||
const seriesAllocationList = ref<ShopSeriesAllocationResponse[]>([])
|
||||
const seriesBindingForm = reactive({
|
||||
series_allocation_id: undefined as number | undefined
|
||||
})
|
||||
const seriesBindingRules = reactive<FormRules>({
|
||||
series_allocation_id: [{ required: true, message: '请选择套餐系列分配', trigger: 'change' }]
|
||||
})
|
||||
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
device_no: '',
|
||||
@@ -665,6 +750,81 @@
|
||||
const handleImportDevice = () => {
|
||||
router.push('/batch/device-import')
|
||||
}
|
||||
|
||||
// 批量设置套餐系列
|
||||
const handleBatchSetSeries = async () => {
|
||||
if (selectedDevices.value.length === 0) {
|
||||
ElMessage.warning('请先选择要设置的设备')
|
||||
return
|
||||
}
|
||||
seriesBindingForm.series_allocation_id = undefined
|
||||
seriesBindingResult.value = null
|
||||
await loadSeriesAllocationList()
|
||||
seriesBindingDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 加载套餐系列分配列表
|
||||
const loadSeriesAllocationList = async () => {
|
||||
seriesLoading.value = true
|
||||
try {
|
||||
const res = await ShopSeriesAllocationService.getShopSeriesAllocations({
|
||||
page: 1,
|
||||
page_size: 1000
|
||||
})
|
||||
if (res.code === 0 && res.data.list) {
|
||||
seriesAllocationList.value = res.data.list
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取套餐系列分配列表失败:', error)
|
||||
ElMessage.error('获取套餐系列分配列表失败')
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认设置套餐系列绑定
|
||||
const handleConfirmSeriesBinding = async () => {
|
||||
if (!seriesBindingFormRef.value) return
|
||||
|
||||
await seriesBindingFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
seriesBindingLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
device_ids: selectedDevices.value.map((d) => d.id),
|
||||
series_allocation_id: seriesBindingForm.series_allocation_id!
|
||||
}
|
||||
const res = await DeviceService.batchSetDeviceSeriesBinding(data)
|
||||
if (res.code === 0) {
|
||||
seriesBindingResult.value = res.data
|
||||
if (res.data.fail_count === 0) {
|
||||
ElMessage.success('全部设置成功')
|
||||
setTimeout(() => {
|
||||
handleCloseSeriesBindingDialog()
|
||||
getTableData()
|
||||
}, 1500)
|
||||
} else {
|
||||
ElMessage.warning(`部分设置失败,请查看失败详情`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('设置套餐系列绑定失败')
|
||||
} finally {
|
||||
seriesBindingLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭套餐系列绑定对话框
|
||||
const handleCloseSeriesBindingDialog = () => {
|
||||
seriesBindingDialogVisible.value = false
|
||||
seriesBindingResult.value = null
|
||||
if (seriesBindingFormRef.value) {
|
||||
seriesBindingFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
642
src/views/asset-management/enterprise-devices/index.vue
Normal file
642
src/views/asset-management/enterprise-devices/index.vue
Normal file
@@ -0,0 +1,642 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="enterprise-devices-page" id="table-full-screen">
|
||||
<!-- 企业信息卡片 -->
|
||||
<ElCard shadow="never" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('enterpriseDevices.title') }}</span>
|
||||
<ElButton @click="goBack">{{ $t('common.cancel') }}</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="3" border v-if="enterpriseInfo">
|
||||
<ElDescriptionsItem label="企业名称">{{
|
||||
enterpriseInfo.enterprise_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="企业编号">{{
|
||||
enterpriseInfo.enterprise_code
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系人">{{ enterpriseInfo.contact_name }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showAllocateDialog">{{
|
||||
$t('enterpriseDevices.buttons.allocateDevices')
|
||||
}}</ElButton>
|
||||
<ElButton
|
||||
type="warning"
|
||||
:disabled="selectedDevices.length === 0"
|
||||
@click="showRecallDialog"
|
||||
>
|
||||
{{ $t('enterpriseDevices.buttons.recallDevices') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="device_id"
|
||||
:loading="loading"
|
||||
:data="deviceList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 授权设备对话框 -->
|
||||
<ElDialog
|
||||
v-model="allocateDialogVisible"
|
||||
:title="$t('enterpriseDevices.dialog.allocateTitle')"
|
||||
width="700px"
|
||||
@close="handleAllocateDialogClose"
|
||||
>
|
||||
<ElForm
|
||||
ref="allocateFormRef"
|
||||
:model="allocateForm"
|
||||
:rules="allocateRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem :label="$t('enterpriseDevices.form.deviceNos')" prop="device_nos">
|
||||
<ElInput
|
||||
v-model="deviceNosText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
:placeholder="$t('enterpriseDevices.form.deviceNosPlaceholder')"
|
||||
@input="handleDeviceNosChange"
|
||||
/>
|
||||
<div style="color: var(--el-color-info); margin-top: 4px; font-size: 12px">
|
||||
{{
|
||||
$t('enterpriseDevices.form.selectedCount', {
|
||||
count: allocateForm.device_nos?.length || 0
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('enterpriseDevices.form.remark')">
|
||||
<ElInput
|
||||
v-model="allocateForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="$t('enterpriseDevices.form.remarkPlaceholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="allocateDialogVisible = false">{{
|
||||
$t('common.cancel')
|
||||
}}</ElButton>
|
||||
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
|
||||
{{ $t('common.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 撤销授权对话框 -->
|
||||
<ElDialog
|
||||
v-model="recallDialogVisible"
|
||||
:title="$t('enterpriseDevices.dialog.recallTitle')"
|
||||
width="600px"
|
||||
@close="handleRecallDialogClose"
|
||||
>
|
||||
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules">
|
||||
<ElFormItem :label="$t('enterpriseDevices.form.selectedDevices')">
|
||||
<div>
|
||||
{{
|
||||
$t('enterpriseDevices.form.selectedCount', {
|
||||
count: selectedDevices.length
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="recallDialogVisible = false">{{ $t('common.cancel') }}</ElButton>
|
||||
<ElButton type="primary" @click="handleRecall" :loading="recallLoading">
|
||||
{{ $t('common.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 结果对话框 -->
|
||||
<ElDialog
|
||||
v-model="resultDialogVisible"
|
||||
:title="$t('enterpriseDevices.dialog.resultTitle')"
|
||||
width="700px"
|
||||
>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem :label="$t('enterpriseDevices.result.successCount')">
|
||||
<ElTag type="success">{{ operationResult.success_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('enterpriseDevices.result.failCount')">
|
||||
<ElTag type="danger">{{ operationResult.fail_count }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 失败项详情 -->
|
||||
<div
|
||||
v-if="operationResult.failed_items && operationResult.failed_items.length > 0"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">{{
|
||||
$t('enterpriseDevices.result.failedItems')
|
||||
}}</ElDivider>
|
||||
<ElTable :data="operationResult.failed_items" border max-height="300">
|
||||
<ElTableColumn
|
||||
prop="device_no"
|
||||
:label="$t('enterpriseDevices.result.deviceNo')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn prop="reason" :label="$t('enterpriseDevices.result.reason')" />
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<!-- 显示已授权设备 -->
|
||||
<div
|
||||
v-if="
|
||||
operationResult.authorized_devices && operationResult.authorized_devices.length > 0
|
||||
"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<ElDivider content-position="left">{{
|
||||
$t('enterpriseDevices.result.authorizedDevices')
|
||||
}}</ElDivider>
|
||||
<ElTable :data="operationResult.authorized_devices" border max-height="200">
|
||||
<ElTableColumn
|
||||
prop="device_no"
|
||||
:label="$t('enterpriseDevices.result.deviceNo')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="device_id"
|
||||
:label="$t('enterpriseDevices.result.deviceId')"
|
||||
width="100"
|
||||
/>
|
||||
<ElTableColumn :label="$t('enterpriseDevices.result.cardCount')">
|
||||
<template #default="{ row }">
|
||||
{{ row.card_count || 0 }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="resultDialogVisible = false">{{
|
||||
$t('common.confirm')
|
||||
}}</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EnterpriseService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type {
|
||||
EnterpriseDeviceItem,
|
||||
AllocateDevicesResponse,
|
||||
RecallDevicesResponse,
|
||||
AuthorizedDeviceItem,
|
||||
FailedDeviceItem
|
||||
} from '@/types/api/enterpriseDevice'
|
||||
import type { EnterpriseItem } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'EnterpriseDevices' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const allocateDialogVisible = ref(false)
|
||||
const allocateLoading = ref(false)
|
||||
const recallDialogVisible = ref(false)
|
||||
const recallLoading = ref(false)
|
||||
const resultDialogVisible = ref(false)
|
||||
const tableRef = ref()
|
||||
const allocateFormRef = ref<FormInstance>()
|
||||
const recallFormRef = ref<FormInstance>()
|
||||
const selectedDevices = ref<EnterpriseDeviceItem[]>([])
|
||||
const enterpriseId = ref<number>(0)
|
||||
const enterpriseInfo = ref<EnterpriseItem | null>(null)
|
||||
const deviceNosText = ref('')
|
||||
const operationResult = ref<AllocateDevicesResponse | RecallDevicesResponse>({
|
||||
success_count: 0,
|
||||
fail_count: 0,
|
||||
failed_items: null,
|
||||
authorized_devices: null
|
||||
})
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
device_no: ''
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
// 授权表单
|
||||
const allocateForm = reactive({
|
||||
device_nos: [] as string[],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 授权表单验证规则
|
||||
const allocateRules = reactive<FormRules>({
|
||||
device_nos: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error(t('enterpriseDevices.validation.deviceNosRequired')))
|
||||
} else if (value.length > 100) {
|
||||
callback(new Error(t('enterpriseDevices.validation.deviceNosMaxLength')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 撤销表单
|
||||
const recallForm = reactive({})
|
||||
|
||||
// 撤销表单验证规则
|
||||
const recallRules = reactive<FormRules>({})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: t('enterpriseDevices.searchForm.deviceNo'),
|
||||
prop: 'device_no',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: t('enterpriseDevices.searchForm.deviceNoPlaceholder')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: t('enterpriseDevices.table.deviceId'), prop: 'device_id' },
|
||||
{ label: t('enterpriseDevices.table.deviceNo'), prop: 'device_no' },
|
||||
{ label: t('enterpriseDevices.table.deviceName'), prop: 'device_name' },
|
||||
{ label: t('enterpriseDevices.table.deviceModel'), prop: 'device_model' },
|
||||
{ label: t('enterpriseDevices.table.cardCount'), prop: 'card_count' },
|
||||
{ label: t('enterpriseDevices.table.authorizedAt'), prop: 'authorized_at' }
|
||||
]
|
||||
|
||||
const deviceList = ref<EnterpriseDeviceItem[]>([])
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'device_id',
|
||||
label: t('enterpriseDevices.table.deviceId'),
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'device_no',
|
||||
label: t('enterpriseDevices.table.deviceNo'),
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'device_name',
|
||||
label: t('enterpriseDevices.table.deviceName'),
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'device_model',
|
||||
label: t('enterpriseDevices.table.deviceModel'),
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'card_count',
|
||||
label: t('enterpriseDevices.table.cardCount'),
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'authorized_at',
|
||||
label: t('enterpriseDevices.table.authorizedAt'),
|
||||
width: 180,
|
||||
formatter: (row: EnterpriseDeviceItem) =>
|
||||
row.authorized_at ? formatDateTime(row.authorized_at) : '-'
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.query.id
|
||||
if (id) {
|
||||
enterpriseId.value = Number(id)
|
||||
getEnterpriseInfo()
|
||||
getTableData()
|
||||
} else {
|
||||
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
|
||||
goBack()
|
||||
}
|
||||
})
|
||||
|
||||
// 获取企业信息
|
||||
const getEnterpriseInfo = async () => {
|
||||
try {
|
||||
const res = await EnterpriseService.getEnterprises({
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
id: enterpriseId.value
|
||||
})
|
||||
if (res.code === 0 && res.data.items && res.data.items.length > 0) {
|
||||
enterpriseInfo.value = res.data.items[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取企业设备列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
device_no: searchForm.device_no || undefined
|
||||
}
|
||||
|
||||
// 清理空值
|
||||
Object.keys(params).forEach((key) => {
|
||||
if (params[key] === '' || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const res = await EnterpriseService.getEnterpriseDevices(enterpriseId.value, params)
|
||||
if (res.code === 0) {
|
||||
deviceList.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 表格选择变化
|
||||
const handleSelectionChange = (selection: EnterpriseDeviceItem[]) => {
|
||||
selectedDevices.value = selection
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 显示授权对话框
|
||||
const showAllocateDialog = () => {
|
||||
allocateDialogVisible.value = true
|
||||
deviceNosText.value = ''
|
||||
allocateForm.device_nos = []
|
||||
allocateForm.remark = ''
|
||||
if (allocateFormRef.value) {
|
||||
allocateFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理设备号输入变化
|
||||
const handleDeviceNosChange = () => {
|
||||
// 解析输入的设备号,支持逗号、空格、换行分隔
|
||||
const deviceNos = deviceNosText.value
|
||||
.split(/[,\s\n]+/)
|
||||
.map((no) => no.trim())
|
||||
.filter((no) => no.length > 0)
|
||||
allocateForm.device_nos = deviceNos
|
||||
}
|
||||
|
||||
// 执行授权
|
||||
const handleAllocate = async () => {
|
||||
if (!allocateFormRef.value) return
|
||||
|
||||
await allocateFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (allocateForm.device_nos.length === 0) {
|
||||
ElMessage.warning(t('enterpriseDevices.messages.deviceNosEmpty'))
|
||||
return
|
||||
}
|
||||
|
||||
if (allocateForm.device_nos.length > 100) {
|
||||
ElMessage.warning(t('enterpriseDevices.messages.deviceNosMaxLimit'))
|
||||
return
|
||||
}
|
||||
|
||||
allocateLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.allocateDevices(enterpriseId.value, {
|
||||
device_nos: allocateForm.device_nos,
|
||||
remark: allocateForm.remark || undefined
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
operationResult.value = res.data
|
||||
allocateDialogVisible.value = false
|
||||
resultDialogVisible.value = true
|
||||
|
||||
// 显示成功消息
|
||||
if (res.data.success_count > 0 && res.data.fail_count === 0) {
|
||||
ElMessage.success(t('enterpriseDevices.messages.allocateSuccess'))
|
||||
} else if (res.data.success_count > 0 && res.data.fail_count > 0) {
|
||||
ElMessage.warning(
|
||||
t('enterpriseDevices.messages.allocatePartialSuccess', {
|
||||
success: res.data.success_count,
|
||||
fail: res.data.fail_count
|
||||
})
|
||||
)
|
||||
} else {
|
||||
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
|
||||
}
|
||||
|
||||
getTableData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
|
||||
} finally {
|
||||
allocateLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭授权对话框
|
||||
const handleAllocateDialogClose = () => {
|
||||
if (allocateFormRef.value) {
|
||||
allocateFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 显示撤销授权对话框
|
||||
const showRecallDialog = () => {
|
||||
if (selectedDevices.value.length === 0) {
|
||||
ElMessage.warning(t('enterpriseDevices.messages.noSelection'))
|
||||
return
|
||||
}
|
||||
recallDialogVisible.value = true
|
||||
if (recallFormRef.value) {
|
||||
recallFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行撤销授权
|
||||
const handleRecall = async () => {
|
||||
if (!recallFormRef.value) return
|
||||
|
||||
ElMessageBox.confirm(
|
||||
t('enterpriseDevices.messages.recallConfirmText', {
|
||||
count: selectedDevices.value.length
|
||||
}),
|
||||
t('enterpriseDevices.messages.recallConfirm'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
recallLoading.value = true
|
||||
try {
|
||||
const res = await EnterpriseService.recallDevices(enterpriseId.value, {
|
||||
device_nos: selectedDevices.value.map((device) => device.device_no)
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
operationResult.value = res.data
|
||||
recallDialogVisible.value = false
|
||||
resultDialogVisible.value = true
|
||||
|
||||
// 显示成功消息
|
||||
if (res.data.success_count > 0 && res.data.fail_count === 0) {
|
||||
ElMessage.success(t('enterpriseDevices.messages.recallSuccess'))
|
||||
} else if (res.data.success_count > 0 && res.data.fail_count > 0) {
|
||||
ElMessage.warning(
|
||||
t('enterpriseDevices.messages.recallPartialSuccess', {
|
||||
success: res.data.success_count,
|
||||
fail: res.data.fail_count
|
||||
})
|
||||
)
|
||||
} else {
|
||||
ElMessage.error(t('enterpriseDevices.messages.recallFailed'))
|
||||
}
|
||||
|
||||
// 清空选择
|
||||
if (tableRef.value) {
|
||||
tableRef.value.clearSelection()
|
||||
}
|
||||
selectedDevices.value = []
|
||||
getTableData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(t('enterpriseDevices.messages.recallFailed'))
|
||||
} finally {
|
||||
recallLoading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 关闭撤销授权对话框
|
||||
const handleRecallDialogClose = () => {
|
||||
if (recallFormRef.value) {
|
||||
recallFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.enterprise-devices-page {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<!-- 新增配置对话框 -->
|
||||
<ElDialog v-model="dialogVisible" title="新增提现配置" width="500px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<ElFormItem label="最低提现金额" prop="min_withdrawal_amount">
|
||||
<ElInputNumber
|
||||
v-model="form.min_withdrawal_amount"
|
||||
@@ -134,17 +134,6 @@
|
||||
/>
|
||||
<div class="form-tip">单位:次</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="到账天数" prop="arrival_days">
|
||||
<ElInputNumber
|
||||
v-model="form.arrival_days"
|
||||
:min="0"
|
||||
:max="30"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">单位:天(0表示实时到账)</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -186,8 +175,7 @@
|
||||
const form = reactive({
|
||||
min_withdrawal_amount: 100,
|
||||
fee_rate: 0.2,
|
||||
daily_withdrawal_limit: 3,
|
||||
arrival_days: 1
|
||||
daily_withdrawal_limit: 3
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
@@ -203,10 +191,6 @@
|
||||
daily_withdrawal_limit: [
|
||||
{ required: true, message: '请输入每日提现次数', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '每日提现次数必须大于0', trigger: 'blur' }
|
||||
],
|
||||
arrival_days: [
|
||||
{ required: true, message: '请输入到账天数', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '到账天数不能为负数', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -314,7 +298,6 @@
|
||||
form.min_withdrawal_amount = 100
|
||||
form.fee_rate = 0.2
|
||||
form.daily_withdrawal_limit = 3
|
||||
form.arrival_days = 1
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
@@ -330,8 +313,7 @@
|
||||
const params = {
|
||||
min_withdrawal_amount: Math.round(form.min_withdrawal_amount * 100), // 元转分
|
||||
fee_rate: Math.round(form.fee_rate * 100), // 百分比转基点
|
||||
daily_withdrawal_limit: form.daily_withdrawal_limit,
|
||||
arrival_days: form.arrival_days
|
||||
daily_withdrawal_limit: form.daily_withdrawal_limit
|
||||
}
|
||||
|
||||
await CommissionService.createWithdrawalSetting(params)
|
||||
|
||||
663
src/views/order-management/order-list/index.vue
Normal file
663
src/views/order-management/order-list/index.vue
Normal file
@@ -0,0 +1,663 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="order-list-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showCreateDialog">{{ t('orderManagement.createOrder') }}</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="orderList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 创建订单对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
:title="t('orderManagement.createOrder')"
|
||||
width="600px"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<ElFormItem :label="t('orderManagement.table.orderType')" prop="order_type">
|
||||
<ElSelect
|
||||
v-model="createForm.order_type"
|
||||
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
:label="t('orderManagement.orderType.singleCard')"
|
||||
value="single_card"
|
||||
/>
|
||||
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('orderManagement.createForm.packageIds')" prop="package_ids">
|
||||
<ElSelect
|
||||
v-model="createForm.package_ids"
|
||||
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<!-- TODO: Load actual packages from API -->
|
||||
<ElOption label="套餐示例" :value="1" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.order_type === 'single_card'"
|
||||
:label="t('orderManagement.createForm.iotCardId')"
|
||||
prop="iot_card_id"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="createForm.iot_card_id"
|
||||
:placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.order_type === 'device'"
|
||||
:label="t('orderManagement.createForm.deviceId')"
|
||||
prop="device_id"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="createForm.device_id"
|
||||
:placeholder="t('orderManagement.createForm.deviceIdPlaceholder')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="createDialogVisible = false">{{
|
||||
t('orderManagement.actions.cancel')
|
||||
}}</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleCreateOrder(createFormRef)"
|
||||
:loading="createLoading"
|
||||
>
|
||||
{{ t('orderManagement.actions.submit') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<ElDialog
|
||||
v-model="detailDialogVisible"
|
||||
:title="t('orderManagement.orderDetail')"
|
||||
width="800px"
|
||||
>
|
||||
<div v-if="currentOrder" class="order-detail">
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.orderNo')">
|
||||
{{ currentOrder.order_no }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.orderType')">
|
||||
{{ getOrderTypeText(currentOrder.order_type) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.paymentStatus')">
|
||||
<ElTag :type="getPaymentStatusType(currentOrder.payment_status)">
|
||||
{{ currentOrder.payment_status_text }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.totalAmount')">
|
||||
{{ formatCurrency(currentOrder.total_amount) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.paymentMethod')">
|
||||
{{ getPaymentMethodText(currentOrder.payment_method) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.buyerType')">
|
||||
{{ getBuyerTypeText(currentOrder.buyer_type) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.commissionStatus')">
|
||||
{{ getCommissionStatusText(currentOrder.commission_status) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.paidAt')">
|
||||
{{ currentOrder.paid_at ? formatDateTime(currentOrder.paid_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.createdAt')">
|
||||
{{ formatDateTime(currentOrder.created_at) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.updatedAt')">
|
||||
{{ formatDateTime(currentOrder.updated_at) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 订单项列表 -->
|
||||
<div v-if="currentOrder.items && currentOrder.items.length > 0" style="margin-top: 20px">
|
||||
<h4>{{ t('orderManagement.orderItems') }}</h4>
|
||||
<ElTable :data="currentOrder.items" border style="margin-top: 10px">
|
||||
<ElTableColumn
|
||||
prop="package_name"
|
||||
:label="t('orderManagement.items.packageName')"
|
||||
min-width="150"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="quantity"
|
||||
:label="t('orderManagement.items.quantity')"
|
||||
width="100"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="unit_price"
|
||||
:label="t('orderManagement.items.unitPrice')"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(row.unit_price) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="amount"
|
||||
:label="t('orderManagement.items.amount')"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(row.amount) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="detailDialogVisible = false">{{
|
||||
t('orderManagement.actions.close')
|
||||
}}</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { OrderService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
Order,
|
||||
OrderQueryParams,
|
||||
CreateOrderRequest,
|
||||
PaymentStatus,
|
||||
OrderType,
|
||||
BuyerType,
|
||||
OrderPaymentMethod,
|
||||
OrderCommissionStatus
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'OrderList' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const createDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentOrder = ref<Order | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: OrderQueryParams = {
|
||||
order_no: '',
|
||||
payment_status: undefined,
|
||||
order_type: undefined,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive<OrderQueryParams>({ ...initialSearchState })
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: t('orderManagement.searchForm.orderNo'),
|
||||
prop: 'order_no',
|
||||
type: 'input',
|
||||
placeholder: t('orderManagement.searchForm.orderNoPlaceholder'),
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('orderManagement.searchForm.paymentStatus'),
|
||||
prop: 'payment_status',
|
||||
type: 'select',
|
||||
placeholder: t('orderManagement.searchForm.paymentStatusPlaceholder'),
|
||||
options: [
|
||||
{ label: t('orderManagement.paymentStatus.pending'), value: 1 },
|
||||
{ label: t('orderManagement.paymentStatus.paid'), value: 2 },
|
||||
{ label: t('orderManagement.paymentStatus.cancelled'), value: 3 },
|
||||
{ label: t('orderManagement.paymentStatus.refunded'), value: 4 }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('orderManagement.searchForm.orderType'),
|
||||
prop: 'order_type',
|
||||
type: 'select',
|
||||
placeholder: t('orderManagement.searchForm.orderTypePlaceholder'),
|
||||
options: [
|
||||
{ label: t('orderManagement.orderType.singleCard'), value: 'single_card' },
|
||||
{ label: t('orderManagement.orderType.device'), value: 'device' }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('orderManagement.searchForm.dateRange'),
|
||||
prop: 'dateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
clearable: true,
|
||||
startPlaceholder: t('orderManagement.searchForm.startDate'),
|
||||
endPlaceholder: t('orderManagement.searchForm.endDate'),
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: t('orderManagement.table.id'), prop: 'id' },
|
||||
{ label: t('orderManagement.table.orderNo'), prop: 'order_no' },
|
||||
{ label: t('orderManagement.table.orderType'), prop: 'order_type' },
|
||||
{ label: t('orderManagement.table.buyerType'), prop: 'buyer_type' },
|
||||
{ label: t('orderManagement.table.paymentStatus'), prop: 'payment_status' },
|
||||
{ label: t('orderManagement.table.totalAmount'), prop: 'total_amount' },
|
||||
{ label: t('orderManagement.table.paymentMethod'), prop: 'payment_method' },
|
||||
{ label: t('orderManagement.table.paidAt'), prop: 'paid_at' },
|
||||
{ label: t('orderManagement.table.createdAt'), prop: 'created_at' },
|
||||
{ label: t('orderManagement.table.operation'), prop: 'operation' }
|
||||
]
|
||||
|
||||
const createFormRef = ref<FormInstance>()
|
||||
|
||||
const createRules = reactive<FormRules>({
|
||||
order_type: [
|
||||
{
|
||||
required: true,
|
||||
message: t('orderManagement.validation.orderTypeRequired'),
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
package_ids: [
|
||||
{
|
||||
required: true,
|
||||
message: t('orderManagement.validation.packageIdsRequired'),
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const createForm = reactive<CreateOrderRequest>({
|
||||
order_type: 'single_card',
|
||||
package_ids: [],
|
||||
iot_card_id: null,
|
||||
device_id: null
|
||||
})
|
||||
|
||||
const orderList = ref<Order[]>([])
|
||||
|
||||
// 格式化货币 - 将分转换为元
|
||||
const formatCurrency = (amount: number): string => {
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
// 获取支付状态标签类型
|
||||
const getPaymentStatusType = (
|
||||
status: PaymentStatus
|
||||
): 'success' | 'info' | 'warning' | 'danger' => {
|
||||
const statusMap: Record<PaymentStatus, 'success' | 'info' | 'warning' | 'danger'> = {
|
||||
1: 'warning', // 待支付
|
||||
2: 'success', // 已支付
|
||||
3: 'info', // 已取消
|
||||
4: 'danger' // 已退款
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取订单类型文本
|
||||
const getOrderTypeText = (type: OrderType): string => {
|
||||
return type === 'single_card'
|
||||
? t('orderManagement.orderType.singleCard')
|
||||
: t('orderManagement.orderType.device')
|
||||
}
|
||||
|
||||
// 获取买家类型文本
|
||||
const getBuyerTypeText = (type: BuyerType): string => {
|
||||
return type === 'personal'
|
||||
? t('orderManagement.buyerType.personal')
|
||||
: t('orderManagement.buyerType.agent')
|
||||
}
|
||||
|
||||
// 获取支付方式文本
|
||||
const getPaymentMethodText = (method: OrderPaymentMethod): string => {
|
||||
const methodMap: Record<OrderPaymentMethod, string> = {
|
||||
wallet: t('orderManagement.paymentMethod.wallet'),
|
||||
wechat: t('orderManagement.paymentMethod.wechat'),
|
||||
alipay: t('orderManagement.paymentMethod.alipay')
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
|
||||
// 获取佣金状态文本
|
||||
const getCommissionStatusText = (status: OrderCommissionStatus): string => {
|
||||
const statusMap: Record<OrderCommissionStatus, string> = {
|
||||
0: t('orderManagement.commissionStatus.notApplicable'),
|
||||
1: t('orderManagement.commissionStatus.pending'),
|
||||
2: t('orderManagement.commissionStatus.settled')
|
||||
}
|
||||
return statusMap[status] || '-'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: t('orderManagement.table.id'),
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'order_no',
|
||||
label: t('orderManagement.table.orderNo'),
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'order_type',
|
||||
label: t('orderManagement.table.orderType'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.order_type === 'single_card' ? 'primary' : 'success' },
|
||||
() => getOrderTypeText(row.order_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'buyer_type',
|
||||
label: t('orderManagement.table.buyerType'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.buyer_type === 'personal' ? 'info' : 'warning' },
|
||||
() => getBuyerTypeText(row.buyer_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'payment_status',
|
||||
label: t('orderManagement.table.paymentStatus'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(ElTag, { type: getPaymentStatusType(row.payment_status) }, () =>
|
||||
row.payment_status_text
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'total_amount',
|
||||
label: t('orderManagement.table.totalAmount'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => formatCurrency(row.total_amount)
|
||||
},
|
||||
{
|
||||
prop: 'payment_method',
|
||||
label: t('orderManagement.table.paymentMethod'),
|
||||
width: 120,
|
||||
formatter: (row: Order) => getPaymentMethodText(row.payment_method)
|
||||
},
|
||||
{
|
||||
prop: 'paid_at',
|
||||
label: t('orderManagement.table.paidAt'),
|
||||
width: 180,
|
||||
formatter: (row: Order) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: t('orderManagement.table.createdAt'),
|
||||
width: 180,
|
||||
formatter: (row: Order) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: t('orderManagement.table.operation'),
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
formatter: (row: Order) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
tooltip: t('orderManagement.actions.viewDetail'),
|
||||
onClick: () => showOrderDetail(row)
|
||||
}),
|
||||
// 只有待支付和已支付的订单可以取消
|
||||
row.payment_status === 1 || row.payment_status === 2
|
||||
? h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
tooltip: t('orderManagement.actions.cancel'),
|
||||
onClick: () => handleCancelOrder(row)
|
||||
})
|
||||
: null
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 获取订单列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: OrderQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
order_no: searchForm.order_no || undefined,
|
||||
payment_status: searchForm.payment_status,
|
||||
order_type: searchForm.order_type,
|
||||
start_time: searchForm.start_time || undefined,
|
||||
end_time: searchForm.end_time || undefined
|
||||
}
|
||||
const res = await OrderService.getOrders(params)
|
||||
if (res.code === 0) {
|
||||
orderList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 处理日期范围
|
||||
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
|
||||
searchForm.start_time = searchForm.dateRange[0]
|
||||
searchForm.end_time = searchForm.dateRange[1]
|
||||
} else {
|
||||
searchForm.start_time = ''
|
||||
searchForm.end_time = ''
|
||||
}
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.page_size = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 显示创建订单对话框
|
||||
const showCreateDialog = () => {
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 对话框关闭后的清理
|
||||
const handleCreateDialogClosed = () => {
|
||||
// 重置表单(会同时清除验证状态)
|
||||
createFormRef.value?.resetFields()
|
||||
|
||||
// 重置表单数据到初始值
|
||||
createForm.order_type = 'single_card'
|
||||
createForm.package_ids = []
|
||||
createForm.iot_card_id = null
|
||||
createForm.device_id = null
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
const handleCreateOrder = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
createLoading.value = true
|
||||
try {
|
||||
const data: CreateOrderRequest = {
|
||||
order_type: createForm.order_type,
|
||||
package_ids: createForm.package_ids,
|
||||
iot_card_id: createForm.order_type === 'single_card' ? createForm.iot_card_id : null,
|
||||
device_id: createForm.order_type === 'device' ? createForm.device_id : null
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
ElMessage.success(t('orderManagement.messages.createSuccess'))
|
||||
createDialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
const showOrderDetail = async (row: Order) => {
|
||||
try {
|
||||
const res = await OrderService.getOrderById(row.id)
|
||||
if (res.code === 0) {
|
||||
currentOrder.value = res.data
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
const handleCancelOrder = (row: Order) => {
|
||||
// 已支付的订单不能取消
|
||||
if (row.payment_status === 2) {
|
||||
ElMessage.warning(t('orderManagement.messages.cannotCancelPaid'))
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
t('orderManagement.messages.cancelConfirmText'),
|
||||
t('orderManagement.messages.cancelConfirm'),
|
||||
{
|
||||
confirmButtonText: t('orderManagement.actions.confirm'),
|
||||
cancelButtonText: t('orderManagement.actions.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
await OrderService.cancelOrder(row.id)
|
||||
ElMessage.success(t('orderManagement.messages.cancelSuccess'))
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消操作
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-list-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.order-detail {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
382
src/views/package-management/my-packages/index.vue
Normal file
382
src/views/package-management/my-packages/index.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="my-packages-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="packageList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="套餐详情" width="600px">
|
||||
<ElDescriptions :column="2" border v-if="currentPackage">
|
||||
<ElDescriptionsItem label="套餐编码">{{
|
||||
currentPackage.package_code
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐名称">{{
|
||||
currentPackage.package_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="所属系列">{{
|
||||
currentPackage.series_name
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐类型">
|
||||
<ElTag :type="getPackageTypeTag(currentPackage.package_type)">{{
|
||||
getPackageTypeLabel(currentPackage.package_type)
|
||||
}}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="流量类型">
|
||||
<ElTag :type="getDataTypeTag(currentPackage.data_type)">{{
|
||||
getDataTypeLabel(currentPackage.data_type)
|
||||
}}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="真流量">{{
|
||||
currentPackage.real_data_mb
|
||||
}}MB</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="虚流量">{{
|
||||
currentPackage.virtual_data_mb
|
||||
}}MB</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="有效期">{{
|
||||
currentPackage.duration_months
|
||||
}}月</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐价格">¥{{
|
||||
(currentPackage.price / 100).toFixed(2)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="上架状态">
|
||||
<ElTag :type="currentPackage.shelf_status === 1 ? 'success' : 'info'">{{
|
||||
currentPackage.shelf_status === 1 ? '上架' : '下架'
|
||||
}}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="currentPackage.status === 1 ? 'success' : 'danger'">{{
|
||||
currentPackage.status === 1 ? '启用' : '禁用'
|
||||
}}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="描述" :span="2">{{
|
||||
currentPackage.description || '无'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { PackageManageService, PackageSeriesService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElDescriptions, ElDescriptionsItem } from 'element-plus'
|
||||
import type { PackageResponse } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
PACKAGE_TYPE_OPTIONS,
|
||||
getPackageTypeLabel,
|
||||
getPackageTypeTag,
|
||||
getDataTypeLabel,
|
||||
getDataTypeTag
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'MyPackages' })
|
||||
|
||||
const loading = ref(false)
|
||||
const seriesLoading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const tableRef = ref()
|
||||
const currentPackage = ref<PackageResponse | null>(null)
|
||||
const seriesOptions = ref<any[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
series_id: undefined as number | undefined,
|
||||
package_type: undefined as string | undefined
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '套餐系列',
|
||||
prop: 'series_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: searchSeries,
|
||||
loading: seriesLoading.value,
|
||||
placeholder: '请选择或搜索套餐系列'
|
||||
},
|
||||
options: () =>
|
||||
seriesOptions.value.map((s: any) => ({
|
||||
label: s.series_name,
|
||||
value: s.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '套餐类型',
|
||||
prop: 'package_type',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择套餐类型'
|
||||
},
|
||||
options: () =>
|
||||
PACKAGE_TYPE_OPTIONS.map((o) => ({
|
||||
label: o.label,
|
||||
value: o.value
|
||||
}))
|
||||
}
|
||||
])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '套餐编码', prop: 'package_code' },
|
||||
{ label: '套餐名称', prop: 'package_name' },
|
||||
{ label: '所属系列', prop: 'series_name' },
|
||||
{ label: '套餐类型', prop: 'package_type' },
|
||||
{ label: '流量类型', prop: 'data_type' },
|
||||
{ label: '真流量', prop: 'real_data_mb' },
|
||||
{ label: '虚流量', prop: 'virtual_data_mb' },
|
||||
{ label: '有效期', prop: 'duration_months' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
const packageList = ref<PackageResponse[]>([])
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'package_code',
|
||||
label: '套餐编码',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'package_name',
|
||||
label: '套餐名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'series_name',
|
||||
label: '所属系列',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'package_type',
|
||||
label: '套餐类型',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getPackageTypeTag(row.package_type), size: 'small' },
|
||||
() => getPackageTypeLabel(row.package_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'data_type',
|
||||
label: '流量类型',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: getDataTypeTag(row.data_type), size: 'small' },
|
||||
() => getDataTypeLabel(row.data_type)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'real_data_mb',
|
||||
label: '真流量',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => `${row.real_data_mb}MB`
|
||||
},
|
||||
{
|
||||
prop: 'virtual_data_mb',
|
||||
label: '虚流量',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => `${row.virtual_data_mb}MB`
|
||||
},
|
||||
{
|
||||
prop: 'duration_months',
|
||||
label: '有效期',
|
||||
width: 100,
|
||||
formatter: (row: PackageResponse) => `${row.duration_months}月`
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
formatter: (row: PackageResponse) => {
|
||||
return h(ArtButtonTable, {
|
||||
text: '查看详情',
|
||||
onClick: () => showDetail(row)
|
||||
})
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadSeriesOptions()
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 加载套餐系列选项(默认加载10条)
|
||||
const loadSeriesOptions = async (seriesName?: string) => {
|
||||
seriesLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 1
|
||||
}
|
||||
if (seriesName) {
|
||||
params.series_name = seriesName
|
||||
}
|
||||
const res = await PackageSeriesService.getPackageSeries(params)
|
||||
if (res.code === 0) {
|
||||
seriesOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载系列选项失败:', error)
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索系列
|
||||
const searchSeries = (query: string) => {
|
||||
if (query) {
|
||||
loadSeriesOptions(query)
|
||||
} else {
|
||||
loadSeriesOptions()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的可售套餐列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
series_id: searchForm.series_id || undefined,
|
||||
package_type: searchForm.package_type || undefined
|
||||
}
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
packageList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.page_size = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 显示详情
|
||||
const showDetail = async (row: PackageResponse) => {
|
||||
try {
|
||||
const res = await PackageManageService.getPackageDetail(row.id)
|
||||
if (res.code === 0) {
|
||||
currentPackage.value = res.data
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取套餐详情失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-packages-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,9 @@
|
||||
<div class="package-series-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
@@ -17,8 +18,7 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showAddDialog">新增</ElButton>
|
||||
<ElButton type="primary" @click="showDialog('add')">新增套餐系列</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -27,12 +27,11 @@
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:data="seriesList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
@@ -41,57 +40,41 @@
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增套餐系列对话框 -->
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="addDialogVisible"
|
||||
title="新增套餐系列"
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增套餐系列' : '编辑套餐系列'"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="addFormRef" :model="addFormData" :rules="addRules" label-width="120px">
|
||||
<ElFormItem label="系列名称" prop="seriesName">
|
||||
<ElInput v-model="addFormData.seriesName" placeholder="请输入系列名称" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="包含套餐" prop="packageNames">
|
||||
<ElSelect
|
||||
v-model="addFormData.packageNames"
|
||||
placeholder="请选择要包含的套餐"
|
||||
style="width: 100%"
|
||||
multiple
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="系列编码" prop="series_code">
|
||||
<ElInput
|
||||
v-model="form.series_code"
|
||||
placeholder="请输入系列编码"
|
||||
:disabled="dialogType === 'edit'"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
|
||||
<ElOption label="随意联畅玩月卡套餐" value="changwan_monthly" />
|
||||
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
|
||||
<ElOption label="如意包月流量包" value="ruyi_monthly" />
|
||||
<ElOption label="Y-NB专享套餐" value="nb_special" />
|
||||
<ElOption label="NB-IoT基础套餐" value="nb_basic" />
|
||||
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
|
||||
<ElOption label="200G超值流量包" value="big_data_200g" />
|
||||
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
|
||||
<ElOption label="广电天翼卡" value="gdtv_tianyi" />
|
||||
</ElSelect>
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系列名称" prop="series_name">
|
||||
<ElInput v-model="form.series_name" placeholder="请输入系列名称" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="系列描述" prop="description">
|
||||
<ElInput
|
||||
v-model="addFormData.description"
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
placeholder="请输入系列描述(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
placeholder="请输入系列描述(可选)"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="addDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAddSubmit" :loading="addLoading">
|
||||
确认新增
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
|
||||
提交
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -103,343 +86,328 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { PackageSeriesService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'PackageSeries' })
|
||||
|
||||
const addDialogVisible = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
seriesName: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const submitLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 新增表单实例
|
||||
const addFormRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const addFormData = reactive({
|
||||
seriesName: '',
|
||||
packageNames: [] as string[],
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
seriesName: '畅玩系列',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00',
|
||||
status: '启用'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
seriesName: '如意系列',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00',
|
||||
status: '启用'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
seriesName: 'NB专享',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00',
|
||||
status: '禁用'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
seriesName: '大流量系列',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00',
|
||||
status: '启用'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
seriesName: '广电系列',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00',
|
||||
status: '禁用'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageSeriesList()
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
series_name: '',
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageSeriesList()
|
||||
}
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '系列名称',
|
||||
prop: 'seriesName',
|
||||
prop: 'series_name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入系列名称'
|
||||
}
|
||||
},
|
||||
onChange: handleFormChange
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '系列名称', prop: 'seriesName' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '系列编码', prop: 'series_code' },
|
||||
{ label: '系列名称', prop: 'series_name' },
|
||||
{ label: '描述', prop: 'description' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '更新时间', prop: 'updated_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '启用':
|
||||
return 'success'
|
||||
case '禁用':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
series_code: [
|
||||
{ required: true, message: '请输入系列编码', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
series_name: [
|
||||
{ required: true, message: '请输入系列名称', trigger: 'blur' },
|
||||
{ min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' }
|
||||
],
|
||||
description: [{ max: 500, message: '描述不能超过 500 个字符', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 显示新增对话框
|
||||
const showAddDialog = () => {
|
||||
addDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (addFormRef.value) {
|
||||
addFormRef.value.resetFields()
|
||||
}
|
||||
addFormData.seriesName = ''
|
||||
addFormData.packageNames = []
|
||||
addFormData.description = ''
|
||||
}
|
||||
// 表单数据
|
||||
const form = reactive<any>({
|
||||
id: 0,
|
||||
series_code: '',
|
||||
series_name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 启用系列
|
||||
const enableSeries = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要启用套餐系列"${row.seriesName}"吗?`, '启用确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('启用成功')
|
||||
getPackageSeriesList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消启用')
|
||||
})
|
||||
}
|
||||
|
||||
// 禁用系列
|
||||
const disableSeries = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要禁用套餐系列"${row.seriesName}"吗?`, '禁用确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('禁用成功')
|
||||
getPackageSeriesList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消禁用')
|
||||
})
|
||||
}
|
||||
|
||||
// 删除系列
|
||||
const deleteSeries = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除套餐系列"${row.seriesName}"吗?删除后将无法恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getPackageSeriesList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
const seriesList = ref<PackageSeriesResponse[]>([])
|
||||
const dialogType = ref('add')
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'seriesName',
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'series_code',
|
||||
label: '系列编码',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'series_name',
|
||||
label: '系列名称',
|
||||
minWidth: 180
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间',
|
||||
width: 160
|
||||
prop: 'description',
|
||||
label: '描述',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
formatter: (row: PackageSeriesResponse) => {
|
||||
const frontendStatus = apiStatusToFrontend(row.status)
|
||||
return h(ElSwitch, {
|
||||
modelValue: frontendStatus,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
inactiveValue: CommonStatus.DISABLED,
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: PackageSeriesResponse) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: PackageSeriesResponse) => formatDateTime(row.updated_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: any) => {
|
||||
const buttons = []
|
||||
|
||||
if (row.status === '启用') {
|
||||
buttons.push(
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
formatter: (row: PackageSeriesResponse) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '禁用',
|
||||
onClick: () => disableSeries(row)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
buttons.push(
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '启用',
|
||||
onClick: () => enableSeries(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
type: 'delete',
|
||||
onClick: () => deleteSeries(row)
|
||||
})
|
||||
)
|
||||
|
||||
return h('div', { class: 'operation-buttons' }, buttons)
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageSeriesList()
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 获取套餐系列列表
|
||||
const getPackageSeriesList = async () => {
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||
const endIndex = startIndex + pagination.pageSize
|
||||
const paginatedData = mockData.slice(startIndex, endIndex)
|
||||
|
||||
tableData.value = paginatedData
|
||||
pagination.total = mockData.length
|
||||
loading.value = false
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
series_name: searchForm.series_name || undefined,
|
||||
status: searchForm.status || undefined
|
||||
}
|
||||
const res = await PackageSeriesService.getPackageSeries(params)
|
||||
if (res.code === 0) {
|
||||
seriesList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取套餐系列列表失败:', error)
|
||||
console.error(error)
|
||||
ElMessage.error('获取套餐系列列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageSeriesList()
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageSeriesList()
|
||||
pagination.page_size = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageSeriesList()
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 新增表单验证规则
|
||||
const addRules = reactive<FormRules>({
|
||||
seriesName: [
|
||||
{ required: true, message: '请输入系列名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '系列名称长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
packageNames: [{ required: true, message: '请选择要包含的套餐', trigger: 'change' }]
|
||||
// 显示新增/编辑对话框
|
||||
const showDialog = (type: string, row?: PackageSeriesResponse) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.id = row.id
|
||||
form.series_code = row.series_code
|
||||
form.series_name = row.series_name
|
||||
form.description = row.description || ''
|
||||
} else {
|
||||
form.id = 0
|
||||
form.series_code = ''
|
||||
form.series_name = ''
|
||||
form.description = ''
|
||||
}
|
||||
|
||||
// 重置表单验证状态
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 提交新增
|
||||
const handleAddSubmit = async () => {
|
||||
if (!addFormRef.value) return
|
||||
// 删除套餐系列
|
||||
const deleteSeries = (row: PackageSeriesResponse) => {
|
||||
ElMessageBox.confirm(`确定删除套餐系列 ${row.series_name} 吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await PackageSeriesService.deletePackageSeries(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
await addFormRef.value.validate((valid) => {
|
||||
// 提交表单
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
addLoading.value = true
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
series_code: form.series_code,
|
||||
series_name: form.series_name,
|
||||
description: form.description || undefined
|
||||
}
|
||||
|
||||
// 模拟新增过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`新增套餐系列成功!系列名称:${addFormData.seriesName},包含套餐:${addFormData.packageNames.length} 个`
|
||||
)
|
||||
addDialogVisible.value = false
|
||||
addLoading.value = false
|
||||
getPackageSeriesList()
|
||||
}, 2000)
|
||||
if (dialogType.value === 'add') {
|
||||
await PackageSeriesService.createPackageSeries(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await PackageSeriesService.updatePackageSeries(form.id, data)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (row: PackageSeriesResponse, newFrontendStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
const newApiStatus = frontendStatusToApi(newFrontendStatus)
|
||||
// 先更新UI(将后端状态转换)
|
||||
row.status = newApiStatus
|
||||
try {
|
||||
await PackageSeriesService.updatePackageSeriesStatus(row.id, newApiStatus)
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
// 切换失败,恢复原状态
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -447,16 +415,7 @@
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
867
src/views/package-management/series-assign/index.vue
Normal file
867
src/views/package-management/series-assign/index.vue
Normal file
@@ -0,0 +1,867 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="series-assign-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showDialog('add')">新增系列分配</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="allocationList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增系列分配' : '编辑系列分配'"
|
||||
width="650px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
|
||||
<!-- 基本信息 -->
|
||||
<ElFormItem label="选择套餐系列" prop="series_id" v-if="dialogType === 'add'">
|
||||
<ElSelect
|
||||
v-model="form.series_id"
|
||||
placeholder="请选择套餐系列"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchSeries"
|
||||
:loading="seriesLoading"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="series in seriesOptions"
|
||||
:key="series.id"
|
||||
:label="`${series.series_name} (${series.series_code})`"
|
||||
:value="series.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选择店铺" prop="shop_id" v-if="dialogType === 'add'">
|
||||
<ElSelect
|
||||
v-model="form.shop_id"
|
||||
placeholder="请选择店铺"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchShop"
|
||||
:loading="shopLoading"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="shop in shopOptions"
|
||||
:key="shop.id"
|
||||
:label="shop.shop_name"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 基础返佣配置 -->
|
||||
<ElDivider content-position="left">基础返佣配置</ElDivider>
|
||||
<ElFormItem label="返佣模式" prop="base_commission.mode">
|
||||
<ElRadioGroup v-model="form.base_commission.mode">
|
||||
<ElRadio value="fixed">固定金额</ElRadio>
|
||||
<ElRadio value="percent">百分比</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="form.base_commission.mode === 'fixed' ? '返佣金额(分)' : '返佣百分比(千分比)'"
|
||||
prop="base_commission.value"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.base_commission.value"
|
||||
:min="0"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
:placeholder="
|
||||
form.base_commission.mode === 'fixed'
|
||||
? '请输入固定返佣金额(分)'
|
||||
: '请输入返佣百分比的千分比(如200表示20%)'
|
||||
"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
{{
|
||||
form.base_commission.mode === 'fixed'
|
||||
? '每笔交易返佣该固定金额(单位:分)'
|
||||
: '返佣百分比的千分比,如200表示20%,即每笔交易返佣 = 交易金额 × 20%'
|
||||
}}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 梯度返佣配置 -->
|
||||
<ElDivider content-position="left">梯度返佣设置(可选)</ElDivider>
|
||||
<ElFormItem label="启用梯度返佣">
|
||||
<ElSwitch v-model="form.enable_tier_commission" />
|
||||
</ElFormItem>
|
||||
|
||||
<template v-if="form.enable_tier_commission">
|
||||
<ElFormItem label="周期类型" prop="tier_config.period_type">
|
||||
<ElSelect v-model="form.tier_config.period_type" placeholder="请选择周期类型" style="width: 100%">
|
||||
<ElOption label="月度" value="monthly" />
|
||||
<ElOption label="季度" value="quarterly" />
|
||||
<ElOption label="年度" value="yearly" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="梯度类型" prop="tier_config.tier_type">
|
||||
<ElSelect v-model="form.tier_config.tier_type" placeholder="请选择梯度类型" style="width: 100%">
|
||||
<ElOption label="按销量" value="sales_count" />
|
||||
<ElOption label="按销售额" value="sales_amount" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="梯度档位">
|
||||
<div class="tier-list">
|
||||
<div v-for="(tier, index) in form.tier_config.tiers" :key="index" class="tier-item">
|
||||
<ElInputNumber
|
||||
v-model="tier.threshold"
|
||||
:min="1"
|
||||
:controls="false"
|
||||
placeholder="阈值"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<ElSelect v-model="tier.mode" placeholder="模式" style="width: 100px">
|
||||
<ElOption label="固定" value="fixed" />
|
||||
<ElOption label="百分比" value="percent" />
|
||||
</ElSelect>
|
||||
<ElInputNumber
|
||||
v-model="tier.value"
|
||||
:min="0"
|
||||
:controls="false"
|
||||
placeholder="返佣值"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<ElButton type="danger" @click="removeTier(index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton type="primary" @click="addTier">添加档位</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)" :loading="submitLoading">
|
||||
提交
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ShopSeriesAllocationService, PackageSeriesService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
ShopSeriesAllocationResponse,
|
||||
PackageSeriesResponse,
|
||||
ShopResponse
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import {
|
||||
CommonStatus,
|
||||
getStatusText,
|
||||
frontendStatusToApi,
|
||||
apiStatusToFrontend
|
||||
} from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'SeriesAssign' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const seriesLoading = ref(false)
|
||||
const shopLoading = ref(false)
|
||||
const tableRef = ref()
|
||||
const formRef = ref<FormInstance>()
|
||||
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const searchSeriesOptions = ref<PackageSeriesResponse[]>([])
|
||||
const searchShopOptions = ref<ShopResponse[]>([])
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
shop_id: undefined as number | undefined,
|
||||
series_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: handleSearchShop,
|
||||
loading: shopLoading.value,
|
||||
placeholder: '请选择或搜索店铺'
|
||||
},
|
||||
options: () =>
|
||||
searchShopOptions.value.map((s) => ({
|
||||
label: s.shop_name,
|
||||
value: s.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '套餐系列',
|
||||
prop: 'series_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: handleSearchSeries,
|
||||
loading: seriesLoading.value,
|
||||
placeholder: '请选择或搜索套餐系列'
|
||||
},
|
||||
options: () =>
|
||||
searchSeriesOptions.value.map((s) => ({
|
||||
label: s.series_name,
|
||||
value: s.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 2 }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '系列名称', prop: 'series_name' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '分配者店铺', prop: 'allocator_shop_name' },
|
||||
{ label: '基础返佣', prop: 'base_commission' },
|
||||
{ label: '梯度返佣', prop: 'enable_tier_commission' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<any>({
|
||||
id: 0,
|
||||
series_id: undefined,
|
||||
shop_id: undefined,
|
||||
base_commission: {
|
||||
mode: 'fixed',
|
||||
value: 0
|
||||
},
|
||||
enable_tier_commission: false,
|
||||
tier_config: {
|
||||
period_type: 'monthly',
|
||||
tier_type: 'sales_count',
|
||||
tiers: []
|
||||
}
|
||||
})
|
||||
|
||||
// 动态验证规则
|
||||
const rules = computed<FormRules>(() => {
|
||||
const baseRules: FormRules = {
|
||||
series_id: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }],
|
||||
'base_commission.mode': [{ required: true, message: '请选择返佣模式', trigger: 'change' }],
|
||||
'base_commission.value': [
|
||||
{ required: true, message: '请输入返佣值', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
callback(new Error('请输入返佣值'))
|
||||
} else if (value < 0) {
|
||||
callback(new Error('返佣值不能小于0'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 如果启用了梯度返佣,添加梯度返佣的验证规则
|
||||
if (form.enable_tier_commission) {
|
||||
baseRules['tier_config.period_type'] = [
|
||||
{ required: true, message: '请选择周期类型', trigger: 'change' }
|
||||
]
|
||||
baseRules['tier_config.tier_type'] = [
|
||||
{ required: true, message: '请选择梯度类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
return baseRules
|
||||
})
|
||||
|
||||
const allocationList = ref<ShopSeriesAllocationResponse[]>([])
|
||||
const dialogType = ref('add')
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'series_name',
|
||||
label: '系列名称',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'allocator_shop_name',
|
||||
label: '分配者店铺',
|
||||
minWidth: 150,
|
||||
formatter: (row: ShopSeriesAllocationResponse) => {
|
||||
return row.allocator_shop_name || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'base_commission',
|
||||
label: '基础返佣',
|
||||
width: 150,
|
||||
formatter: (row: ShopSeriesAllocationResponse) => {
|
||||
if (!row.base_commission) return '-'
|
||||
const { mode, value } = row.base_commission
|
||||
if (mode === 'fixed') {
|
||||
return `固定 ¥${(value / 100).toFixed(2)}`
|
||||
} else {
|
||||
return `百分比 ${(value / 10).toFixed(1)}%`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'enable_tier_commission',
|
||||
label: '梯度返佣',
|
||||
width: 100,
|
||||
formatter: (row: ShopSeriesAllocationResponse) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.enable_tier_commission ? 'success' : 'info', size: 'small' },
|
||||
() => (row.enable_tier_commission ? '已启用' : '未启用')
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: ShopSeriesAllocationResponse) => {
|
||||
const frontendStatus = apiStatusToFrontend(row.status)
|
||||
return h(ElSwitch, {
|
||||
modelValue: frontendStatus,
|
||||
activeValue: CommonStatus.ENABLED,
|
||||
inactiveValue: CommonStatus.DISABLED,
|
||||
activeText: getStatusText(CommonStatus.ENABLED),
|
||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||
inlinePrompt: true,
|
||||
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||
handleStatusChange(row, val as number)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: ShopSeriesAllocationResponse) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
formatter: (row: ShopSeriesAllocationResponse) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteAllocation(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadSeriesOptions()
|
||||
loadShopOptions()
|
||||
loadSearchSeriesOptions()
|
||||
loadSearchShopOptions()
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// 加载系列选项(用于新增对话框,默认加载10条)
|
||||
const loadSeriesOptions = async (seriesName?: string) => {
|
||||
seriesLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 1
|
||||
}
|
||||
if (seriesName) {
|
||||
params.series_name = seriesName
|
||||
}
|
||||
const res = await PackageSeriesService.getPackageSeries(params)
|
||||
if (res.code === 0) {
|
||||
seriesOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载系列选项失败:', error)
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载店铺选项(用于新增对话框,默认加载10条)
|
||||
const loadShopOptions = async (shopName?: string) => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 10
|
||||
}
|
||||
if (shopName) {
|
||||
params.shop_name = shopName
|
||||
}
|
||||
const res = await ShopService.getShops(params)
|
||||
if (res.code === 0) {
|
||||
shopOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载店铺选项失败:', error)
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载搜索栏系列选项(默认加载10条)
|
||||
const loadSearchSeriesOptions = async () => {
|
||||
try {
|
||||
const res = await PackageSeriesService.getPackageSeries({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 1
|
||||
})
|
||||
if (res.code === 0) {
|
||||
searchSeriesOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载搜索栏系列选项失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载搜索栏店铺选项(默认加载10条)
|
||||
const loadSearchShopOptions = async () => {
|
||||
try {
|
||||
const res = await ShopService.getShops({ page: 1, page_size: 10 })
|
||||
if (res.code === 0) {
|
||||
searchShopOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载搜索栏店铺选项失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索系列(用于新增对话框)
|
||||
const searchSeries = (query: string) => {
|
||||
if (query) {
|
||||
loadSeriesOptions(query)
|
||||
} else {
|
||||
loadSeriesOptions()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索店铺(用于新增对话框)
|
||||
const searchShop = (query: string) => {
|
||||
if (query) {
|
||||
loadShopOptions(query)
|
||||
} else {
|
||||
loadShopOptions()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索系列(用于搜索栏)
|
||||
const handleSearchSeries = async (query: string) => {
|
||||
if (!query) {
|
||||
loadSearchSeriesOptions()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await PackageSeriesService.getPackageSeries({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
series_name: query,
|
||||
status: 1
|
||||
})
|
||||
if (res.code === 0) {
|
||||
searchSeriesOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索系列失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索店铺(用于搜索栏)
|
||||
const handleSearchShop = async (query: string) => {
|
||||
if (!query) {
|
||||
loadSearchShopOptions()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await ShopService.getShops({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
shop_name: query
|
||||
})
|
||||
if (res.code === 0) {
|
||||
searchShopOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索店铺失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分配列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
shop_id: searchForm.shop_id || undefined,
|
||||
series_id: searchForm.series_id || undefined,
|
||||
status: searchForm.status || undefined
|
||||
}
|
||||
const res = await ShopSeriesAllocationService.getShopSeriesAllocations(params)
|
||||
if (res.code === 0) {
|
||||
allocationList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取系列分配列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.page_size = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 添加档位
|
||||
const addTier = () => {
|
||||
form.tier_config.tiers.push({
|
||||
threshold: 0,
|
||||
mode: 'fixed',
|
||||
value: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 删除档位
|
||||
const removeTier = (index: number) => {
|
||||
form.tier_config.tiers.splice(index, 1)
|
||||
}
|
||||
|
||||
// 显示新增/编辑对话框
|
||||
const showDialog = (type: string, row?: ShopSeriesAllocationResponse) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.id = row.id
|
||||
form.series_id = row.series_id
|
||||
form.shop_id = row.shop_id
|
||||
form.base_commission = {
|
||||
mode: row.base_commission.mode,
|
||||
value: row.base_commission.value
|
||||
}
|
||||
form.enable_tier_commission = row.enable_tier_commission
|
||||
if (row.enable_tier_commission && row.tier_config) {
|
||||
form.tier_config = {
|
||||
period_type: row.tier_config.period_type,
|
||||
tier_type: row.tier_config.tier_type,
|
||||
tiers: row.tier_config.tiers.map((t) => ({ ...t }))
|
||||
}
|
||||
} else {
|
||||
form.tier_config = {
|
||||
period_type: 'monthly',
|
||||
tier_type: 'sales_count',
|
||||
tiers: []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
form.id = 0
|
||||
form.series_id = undefined
|
||||
form.shop_id = undefined
|
||||
form.base_commission = {
|
||||
mode: 'fixed',
|
||||
value: 0
|
||||
}
|
||||
form.enable_tier_commission = false
|
||||
form.tier_config = {
|
||||
period_type: 'monthly',
|
||||
tier_type: 'sales_count',
|
||||
tiers: []
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单验证状态
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 处理弹窗关闭事件
|
||||
const handleDialogClosed = () => {
|
||||
// 清除表单验证状态
|
||||
formRef.value?.clearValidate()
|
||||
// 重置表单数据
|
||||
form.id = 0
|
||||
form.series_id = undefined
|
||||
form.shop_id = undefined
|
||||
form.base_commission = {
|
||||
mode: 'fixed',
|
||||
value: 0
|
||||
}
|
||||
form.enable_tier_commission = false
|
||||
form.tier_config = {
|
||||
period_type: 'monthly',
|
||||
tier_type: 'sales_count',
|
||||
tiers: []
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分配
|
||||
const deleteAllocation = (row: ShopSeriesAllocationResponse) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定删除系列 ${row.series_name} 对店铺 ${row.shop_name} 的分配吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
await ShopSeriesAllocationService.deleteShopSeriesAllocation(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
// 验证梯度档位
|
||||
if (form.enable_tier_commission) {
|
||||
if (form.tier_config.tiers.length === 0) {
|
||||
ElMessage.warning('启用梯度返佣时至少需要添加一个档位')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证档位阈值递增
|
||||
const thresholds = form.tier_config.tiers.map((t: any) => t.threshold)
|
||||
for (let i = 1; i < thresholds.length; i++) {
|
||||
if (thresholds[i] <= thresholds[i - 1]) {
|
||||
ElMessage.warning('档位阈值必须递增')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
base_commission: {
|
||||
mode: form.base_commission.mode,
|
||||
value: form.base_commission.value
|
||||
},
|
||||
enable_tier_commission: form.enable_tier_commission
|
||||
}
|
||||
|
||||
// 如果启用了梯度返佣,加入梯度配置
|
||||
if (form.enable_tier_commission) {
|
||||
data.tier_config = {
|
||||
period_type: form.tier_config.period_type,
|
||||
tier_type: form.tier_config.tier_type,
|
||||
tiers: form.tier_config.tiers.map((t: any) => ({
|
||||
threshold: t.threshold,
|
||||
mode: t.mode,
|
||||
value: t.value
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
data.series_id = form.series_id
|
||||
data.shop_id = form.shop_id
|
||||
await ShopSeriesAllocationService.createShopSeriesAllocation(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await ShopSeriesAllocationService.updateShopSeriesAllocation(form.id, data)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (
|
||||
row: ShopSeriesAllocationResponse,
|
||||
newFrontendStatus: number
|
||||
) => {
|
||||
const oldStatus = row.status
|
||||
const newApiStatus = frontendStatusToApi(newFrontendStatus)
|
||||
row.status = newApiStatus
|
||||
try {
|
||||
await ShopSeriesAllocationService.updateShopSeriesAllocationStatus(row.id, newApiStatus)
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.series-assign-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tier-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tier-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user