Compare commits
2 Commits
6a6d3dd394
...
cc9d2a85a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc9d2a85a2 | ||
|
|
d1c6588d8f |
@@ -153,7 +153,6 @@
|
||||
- [ ] 删除/禁用平台账号
|
||||
- [ ] 重置密码
|
||||
- [ ] 页面开发
|
||||
- [ ] `src/views/account-management/platform-account/index.vue`
|
||||
- [ ] 账号列表(支持搜索、筛选)
|
||||
- [ ] 新增/编辑账号对话框
|
||||
- [ ] 角色分配
|
||||
@@ -173,7 +172,6 @@
|
||||
- [ ] 删除客户角色
|
||||
- [ ] 设置能力边界
|
||||
- [ ] 页面开发
|
||||
- [ ] `src/views/account-management/customer-role/index.vue`
|
||||
- [ ] 角色列表
|
||||
- [ ] 能力边界配置(功能权限、资源限制)
|
||||
- [ ] 组件开发
|
||||
@@ -197,7 +195,6 @@
|
||||
- [ ] 查看代理商详情
|
||||
- [ ] 获取代理商下级列表
|
||||
- [ ] 页面开发
|
||||
- [ ] `src/views/account-management/agent-management/index.vue`
|
||||
- [ ] 代理商树形列表
|
||||
- [ ] 新增/编辑代理商对话框
|
||||
- [ ] 代理商详情页面
|
||||
@@ -238,7 +235,6 @@
|
||||
- [ ] 重置密码
|
||||
- [ ] 禁用/启用账号
|
||||
- [ ] 页面开发
|
||||
- [ ] `src/views/account-management/customer-account/index.vue`
|
||||
- [ ] 账号列表(支持筛选)
|
||||
- [ ] 账号操作(解绑、重置密码等)
|
||||
- [ ] Mock 数据
|
||||
@@ -259,7 +255,6 @@
|
||||
- [ ] 查看佣金详情
|
||||
- [ ] 查看提现记录
|
||||
- [ ] 页面开发
|
||||
- [ ] `src/views/finance-management/customer-account/index.vue`
|
||||
- [ ] 客户账户列表
|
||||
- [ ] 佣金详情展示
|
||||
- [ ] 提现记录列表
|
||||
@@ -315,7 +310,6 @@
|
||||
- [ ] 获取佣金明细
|
||||
- [ ] 申请提现
|
||||
- [ ] 页面开发
|
||||
- [ ] `src/views/finance-management/my-account/index.vue`
|
||||
- [ ] 佣金概览(卡片展示,非列表)
|
||||
- [ ] 佣金明细列表
|
||||
- [ ] 提现申请对话框
|
||||
@@ -453,7 +447,6 @@
|
||||
- [ ] 编辑套餐
|
||||
- [ ] 删除套餐
|
||||
- [ ] 页面开发
|
||||
- [ ] 重构 `src/views/package-management/package-create/index.vue`
|
||||
- [ ] 套餐列表(支持角色过滤)
|
||||
- [ ] 新增/编辑套餐对话框
|
||||
- [ ] Mock 数据
|
||||
@@ -502,7 +495,6 @@
|
||||
- [ ] 续充
|
||||
- [ ] 设备操作
|
||||
- [ ] 页面开发
|
||||
- [ ] 重构 `src/views/card-management/single-card/index.vue`
|
||||
- [ ] 单卡信息展示(描述列表)
|
||||
- [ ] 操作按钮组
|
||||
- [ ] 各类操作对话框
|
||||
@@ -521,7 +513,6 @@
|
||||
- [ ] 获取网卡列表
|
||||
- [ ] 批量操作入口
|
||||
- [ ] 页面开发
|
||||
- [ ] 重构 `src/views/card-management/card-list/index.vue`
|
||||
- [ ] 网卡列表
|
||||
- [ ] 批量操作按钮
|
||||
- [ ] Mock 数据
|
||||
@@ -581,7 +572,6 @@
|
||||
- [ ] 处理换卡申请
|
||||
- [ ] 填充新 ICCID
|
||||
- [ ] 页面开发
|
||||
- [ ] 重构 `src/views/card-management/card-replacement/index.vue`
|
||||
- [ ] 申请列表
|
||||
- [ ] 处理对话框
|
||||
- [ ] Mock 数据
|
||||
@@ -637,7 +627,6 @@
|
||||
- [ ] 查看批量充值记录
|
||||
- [ ] Excel 批量充值导入
|
||||
- [ ] 页面开发
|
||||
- [ ] 重构 `src/views/card-management/offline-batch-recharge/index.vue`
|
||||
- [ ] 充值记录列表
|
||||
- [ ] 批量充值 Excel 导入
|
||||
- [ ] Mock 数据
|
||||
|
||||
@@ -61,22 +61,6 @@
|
||||
- 编辑: `account:edit`
|
||||
- 删除: `account:delete`
|
||||
|
||||
- [x] **平台账号页面** (`/account-management/platform-account`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增平台账号"按钮添加 `v-permission="'platform_account:add'"`
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||
- 分配角色: `platform_account:patch_role`
|
||||
- 修改密码: `platform_account:update_psd`
|
||||
- 编辑: `platform_account:edit`
|
||||
- 删除: `platform_account:delete`
|
||||
|
||||
- [x] **代理账号页面** (`/account-management/shop-account`)
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增代理账号"按钮添加 `v-permission="'shop_account:add'"`
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||
- 修改密码: `shop_account:update_psd`
|
||||
- 编辑: `shop_account:edit`
|
||||
|
||||
### 4. 资产管理模块 (Asset Management) ✅
|
||||
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
|
||||
- [ ] 3.1 Update customer account types in `src/types/api/account.ts`
|
||||
- [ ] 3.2 Update AccountService in `src/api/modules/account.ts` with new endpoints
|
||||
- [ ] 3.3 Update customer account list page `/account-management/customer-account`
|
||||
- [ ] 3.4 Add create/edit customer account forms with user type selection
|
||||
- [ ] 3.5 Add password reset functionality
|
||||
- [ ] 3.6 Add status toggle functionality
|
||||
- [ ] 3.3 Add create/edit customer account forms with user type selection
|
||||
- [ ] 3.4 Add password reset functionality
|
||||
- [ ] 3.5 Add status toggle functionality
|
||||
|
||||
## 4. Enterprise Customer Enhancement
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Change: 客户账户管理功能
|
||||
|
||||
## Why
|
||||
|
||||
运营平台需要统一查看和管理所有客户(包括代理商和企业客户)的账户佣金情况,包括佣金总额、可提现金额、待入账金额、已提现金额等财务数据。目前系统缺少集中的客户账户财务视图,运营人员无法高效地了解客户的佣金和提现状况。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增客户账户管理页面(`src/views/finance/customer-account/index.vue`)
|
||||
- 提供客户账户列表查询功能,支持按客户账号、客户名称、客户类型筛选
|
||||
- 展示客户的佣金相关财务数据:
|
||||
- 佣金总额
|
||||
- 可提现金额
|
||||
- 待入账金额
|
||||
- 已提现金额
|
||||
- 提现次数
|
||||
- 最后提现时间
|
||||
- 提供查看客户账户详情功能
|
||||
- 提供查看客户流水记录功能(入口)
|
||||
- 添加国际化支持(中英文)
|
||||
- 添加路由配置(财务模块下)
|
||||
|
||||
## Impact
|
||||
|
||||
- 新增规范:`specs/customer-account-management/spec.md`
|
||||
- 新增文件:
|
||||
- `src/views/finance/customer-account/index.vue`
|
||||
- 修改文件:
|
||||
- `src/router/routes/asyncRoutes.ts`(添加路由)
|
||||
- `src/router/routesAlias.ts`(添加路由别名)
|
||||
- `src/locales/langs/zh.json`(添加中文翻译)
|
||||
- `src/locales/langs/en.json`(添加英文翻译)
|
||||
- `docs/功能.md`(文档更新)
|
||||
- 依赖模块:
|
||||
- Element Plus 组件(ElTable, ElForm, ElDialog 等)
|
||||
- 现有的 ArtTable 组件
|
||||
- 后端 API(待对接)
|
||||
@@ -1,176 +0,0 @@
|
||||
# 客户账户管理规范
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 客户账户列表查询
|
||||
|
||||
系统 SHALL 提供客户账户列表查询功能,运营人员可以查看所有客户的账户财务信息。
|
||||
|
||||
#### Scenario: 查询所有客户账户
|
||||
|
||||
- **WHEN** 运营人员访问客户账户管理页面
|
||||
- **THEN** 系统显示所有客户账户列表,包含以下字段:
|
||||
- 客户账号
|
||||
- 客户名称
|
||||
- 客户类型(代理商/企业客户)
|
||||
- 佣金总额
|
||||
- 可提现金额
|
||||
- 待入账金额
|
||||
- 已提现金额
|
||||
- 提现次数
|
||||
- 最后提现时间
|
||||
|
||||
#### Scenario: 按客户账号搜索
|
||||
|
||||
- **WHEN** 运营人员在搜索框输入客户账号并点击查询
|
||||
- **THEN** 系统返回匹配该账号的客户记录
|
||||
|
||||
#### Scenario: 按客户名称搜索
|
||||
|
||||
- **WHEN** 运营人员在搜索框输入客户名称并点击查询
|
||||
- **THEN** 系统返回名称包含该关键词的客户记录
|
||||
|
||||
#### Scenario: 按客户类型筛选
|
||||
|
||||
- **WHEN** 运营人员选择客户类型(代理商或企业客户)并点击查询
|
||||
- **THEN** 系统返回该类型的所有客户记录
|
||||
|
||||
#### Scenario: 组合条件搜索
|
||||
|
||||
- **WHEN** 运营人员同时使用多个搜索条件(如账号 + 类型)并点击查询
|
||||
- **THEN** 系统返回同时满足所有条件的客户记录
|
||||
|
||||
#### Scenario: 重置搜索条件
|
||||
|
||||
- **WHEN** 运营人员点击重置按钮
|
||||
- **THEN** 系统清空所有搜索条件并显示完整列表
|
||||
|
||||
### Requirement: 分页功能
|
||||
|
||||
系统 SHALL 支持客户账户列表的分页展示,以提高大数据量下的性能和用户体验。
|
||||
|
||||
#### Scenario: 默认分页显示
|
||||
|
||||
- **WHEN** 运营人员首次访问页面
|
||||
- **THEN** 系统默认显示第 1 页,每页 20 条记录
|
||||
|
||||
#### Scenario: 切换页码
|
||||
|
||||
- **WHEN** 运营人员点击分页器的页码
|
||||
- **THEN** 系统跳转到对应页面并加载数据
|
||||
|
||||
#### Scenario: 调整每页显示数量
|
||||
|
||||
- **WHEN** 运营人员选择不同的每页显示数量(10/20/50/100)
|
||||
- **THEN** 系统重新加载数据并按新的数量显示
|
||||
|
||||
#### Scenario: 显示总记录数
|
||||
|
||||
- **WHEN** 数据加载完成后
|
||||
- **THEN** 分页器显示总记录数
|
||||
|
||||
### Requirement: 客户账户详情查看
|
||||
|
||||
系统 SHALL 提供客户账户详情查看功能,展示客户的完整财务信息。
|
||||
|
||||
#### Scenario: 打开详情对话框
|
||||
|
||||
- **WHEN** 运营人员点击某个客户的"查看详情"按钮
|
||||
- **THEN** 系统弹出详情对话框,展示以下信息:
|
||||
- 客户账号
|
||||
- 客户名称
|
||||
- 客户类型
|
||||
- 联系电话
|
||||
- 佣金总额
|
||||
- 可提现金额
|
||||
- 待入账金额
|
||||
- 已提现金额
|
||||
- 提现次数
|
||||
- 最后提现时间
|
||||
- 注册时间
|
||||
- 备注
|
||||
|
||||
#### Scenario: 关闭详情对话框
|
||||
|
||||
- **WHEN** 运营人员点击对话框关闭按钮或遮罩层
|
||||
- **THEN** 系统关闭详情对话框
|
||||
|
||||
### Requirement: 客户流水记录入口
|
||||
|
||||
系统 SHALL 提供客户流水记录查看入口,方便运营人员查看客户的佣金流水明细。
|
||||
|
||||
#### Scenario: 触发流水记录查看
|
||||
|
||||
- **WHEN** 运营人员点击某个客户的"流水记录"按钮
|
||||
- **THEN** 系统导航到该客户的流水记录页面或打开流水记录对话框
|
||||
|
||||
### Requirement: 数据展示样式
|
||||
|
||||
系统 SHALL 使用不同的视觉样式区分不同类型的数据,提升可读性。
|
||||
|
||||
#### Scenario: 客户类型标签样式
|
||||
|
||||
- **WHEN** 列表或详情中显示客户类型
|
||||
- **THEN** 代理商显示为绿色标签,企业客户显示为蓝色标签
|
||||
|
||||
#### Scenario: 金额颜色区分
|
||||
|
||||
- **WHEN** 列表或详情中显示金额数据
|
||||
- **THEN** 可提现金额使用绿色高亮,待入账金额使用橙色高亮
|
||||
|
||||
#### Scenario: 金额格式化
|
||||
|
||||
- **WHEN** 系统显示金额数据
|
||||
- **THEN** 金额显示为货币格式,保留两位小数,前缀人民币符号"¥"
|
||||
|
||||
### Requirement: 国际化支持
|
||||
|
||||
系统 SHALL 支持中英文双语界面,所有文案通过国际化文件管理。
|
||||
|
||||
#### Scenario: 中文界面显示
|
||||
|
||||
- **WHEN** 系统语言设置为中文
|
||||
- **THEN** 所有界面文案显示为中文
|
||||
|
||||
#### Scenario: 英文界面显示
|
||||
|
||||
- **WHEN** 系统语言设置为英文
|
||||
- **THEN** 所有界面文案显示为英文
|
||||
|
||||
### Requirement: 数据权限控制
|
||||
|
||||
系统 SHALL 根据运营人员的权限角色,控制其可见的客户账户范围。
|
||||
|
||||
#### Scenario: 管理员权限
|
||||
|
||||
- **WHEN** 管理员访问客户账户页面
|
||||
- **THEN** 系统显示所有客户账户
|
||||
|
||||
#### Scenario: 普通运营人员权限
|
||||
|
||||
- **WHEN** 普通运营人员访问客户账户页面
|
||||
- **THEN** 系统仅显示其权限范围内的客户账户
|
||||
|
||||
### Requirement: 异常处理与用户反馈
|
||||
|
||||
系统 SHALL 在操作过程中提供清晰的用户反馈和错误处理。
|
||||
|
||||
#### Scenario: 查询成功反馈
|
||||
|
||||
- **WHEN** 用户执行搜索操作且成功返回结果
|
||||
- **THEN** 系统显示成功消息提示
|
||||
|
||||
#### Scenario: 查询失败处理
|
||||
|
||||
- **WHEN** API 请求失败或超时
|
||||
- **THEN** 系统显示错误消息,并提示用户重试
|
||||
|
||||
#### Scenario: 数据加载状态
|
||||
|
||||
- **WHEN** 系统正在加载数据
|
||||
- **THEN** 显示加载动画或骨架屏,防止用户误操作
|
||||
|
||||
#### Scenario: 空数据提示
|
||||
|
||||
- **WHEN** 查询结果为空
|
||||
- **THEN** 系统显示"暂无数据"提示
|
||||
@@ -1,58 +0,0 @@
|
||||
# 实现任务清单
|
||||
|
||||
## 1. 前端页面实现
|
||||
|
||||
- [x] 1.1 创建客户账户管理页面组件 `src/views/finance/customer-account/index.vue`
|
||||
- [x] 1.1.1 实现搜索表单(客户账号、客户名称、客户类型)
|
||||
- [x] 1.1.2 实现数据表格展示(使用 ArtTable)
|
||||
- [x] 1.1.3 实现分页功能
|
||||
- [x] 1.1.4 实现详情对话框
|
||||
- [x] 1.1.5 添加操作按钮(查看详情、流水记录)
|
||||
- [x] 1.2 添加路由配置
|
||||
- [x] 1.2.1 在 `src/router/routes/asyncRoutes.ts` 中添加路由
|
||||
- [x] 1.2.2 在 `src/router/routesAlias.ts` 中添加路由别名
|
||||
- [x] 1.3 添加国际化支持
|
||||
- [x] 1.3.1 在 `src/locales/langs/zh.json` 中添加中文文案
|
||||
- [x] 1.3.2 在 `src/locales/langs/en.json` 中添加英文文案
|
||||
|
||||
## 2. API 集成(待实现)
|
||||
|
||||
- [ ] 2.1 创建 API 模块 `src/api/modules/customerAccountApi.ts`
|
||||
- [ ] 2.1.1 实现客户账户列表查询接口
|
||||
- [ ] 2.1.2 实现客户账户详情查询接口
|
||||
- [ ] 2.1.3 实现客户流水记录查询接口
|
||||
- [ ] 2.2 定义 TypeScript 类型
|
||||
- [ ] 2.2.1 定义客户账户数据类型 `CustomerAccount`
|
||||
- [ ] 2.2.2 定义搜索参数类型 `CustomerAccountSearchParams`
|
||||
- [ ] 2.2.3 定义流水记录类型 `CustomerAccountFlow`
|
||||
- [ ] 2.3 替换页面中的模拟数据为真实 API 调用
|
||||
|
||||
## 3. 流水记录功能(待实现)
|
||||
|
||||
- [ ] 3.1 创建流水记录子页面或对话框
|
||||
- [ ] 3.2 实现流水记录列表展示
|
||||
- [ ] 3.3 实现流水记录筛选和分页
|
||||
|
||||
## 4. 权限控制(待实现)
|
||||
|
||||
- [ ] 4.1 配置页面访问权限(后台权限系统)
|
||||
- [ ] 4.2 添加按钮级权限控制(如需要)
|
||||
|
||||
## 5. 测试与优化
|
||||
|
||||
- [ ] 5.1 单元测试(工具函数、业务逻辑)
|
||||
- [ ] 5.2 集成测试(API 调用)
|
||||
- [ ] 5.3 性能优化(大数据量处理)
|
||||
- [ ] 5.4 用户体验优化(加载状态、错误处理)
|
||||
|
||||
## 6. 文档更新
|
||||
|
||||
- [x] 6.1 更新 `docs/功能.md` 功能列表
|
||||
|
||||
## 当前状态
|
||||
|
||||
- ✅ 第 1 阶段(前端页面框架)已完成,使用模拟数据
|
||||
- ⏳ 第 2 阶段(API 集成)待实现
|
||||
- ⏳ 第 3 阶段(流水记录)待实现
|
||||
- ⏳ 第 4 阶段(权限控制)待实现
|
||||
- ⏳ 第 5 阶段(测试优化)待实现
|
||||
@@ -1,309 +0,0 @@
|
||||
# Device Management Design
|
||||
|
||||
## Context
|
||||
|
||||
The system currently has a basic device-import page but lacks comprehensive device management capabilities. Devices are critical assets that need to be tracked throughout their lifecycle - from import, allocation to shops, binding with SIM cards, to eventual recall. This change adds full device management to complement the existing card management features.
|
||||
|
||||
Key integration points:
|
||||
|
||||
- **Existing Card System**: Devices must bind with cards from the existing card-list module
|
||||
- **Shop System**: Devices are allocated to shops using the existing ShopService
|
||||
- **Object Storage**: Imports use the existing StorageService for large file uploads
|
||||
- **Task System**: Import tasks must integrate with the existing task-management infrastructure
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Complete Lifecycle Management**: Support all device operations from import to deletion
|
||||
2. **Seamless Integration**: Reuse existing UI patterns, services, and components
|
||||
3. **Scalable Import**: Handle large CSV imports efficiently using object storage
|
||||
4. **Operation Traceability**: Track all device operations through task management
|
||||
5. **Consistent UX**: Match the look and feel of existing card-list and enterprise-customer pages
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Decision 1: Three-Step Object Storage Import
|
||||
|
||||
**Choice**: Use StorageService.getUploadUrl → uploadFile → DeviceService.importDevices
|
||||
|
||||
**Rationale**:
|
||||
|
||||
- Handles large files (thousands of devices) without timeout issues
|
||||
- Decouples upload from processing (backend can process asynchronously)
|
||||
- Consistent with modern cloud architecture patterns
|
||||
- Allows progress tracking through task management
|
||||
|
||||
**Alternatives Considered**:
|
||||
|
||||
- Direct multipart upload to backend (rejected: not scalable for large files)
|
||||
- Two-step process without pre-signed URL (rejected: less secure, more backend load)
|
||||
|
||||
**Implementation Notes**:
|
||||
|
||||
- Frontend only handles upload to object storage, not file parsing
|
||||
- Backend processes the file asynchronously and creates task records
|
||||
- Task management provides visibility into import progress
|
||||
|
||||
### Decision 2: Device-Card Binding Model
|
||||
|
||||
**Choice**: Store binding with explicit slot_position (1-4) in device_cards table
|
||||
|
||||
**Rationale**:
|
||||
|
||||
- IoT devices have physical SIM slots that need explicit identification
|
||||
- Each device can have 1-4 slots (max_sim_slots)
|
||||
- One card can only bind to one device slot (enforced by backend)
|
||||
- Slot position is critical for physical device configuration
|
||||
|
||||
**Alternatives Considered**:
|
||||
|
||||
- Auto-assign slot positions (rejected: operators need to know physical slot numbers)
|
||||
- Allow one card to bind to multiple devices (rejected: not realistic for physical SIMs)
|
||||
|
||||
**Implementation Notes**:
|
||||
|
||||
- Device detail page shows a 4-slot grid (empty slots show "Bind Card" button)
|
||||
- Binding dialog requires explicit slot selection
|
||||
- Unbinding updates bound_card_count and frees the slot
|
||||
|
||||
### Decision 3: Unified Task Management
|
||||
|
||||
**Choice**: Extend existing task-management to support device import tasks
|
||||
|
||||
**Rationale**:
|
||||
|
||||
- Reuses existing task infrastructure
|
||||
- Provides consistent UX for all import operations
|
||||
- Avoids duplicate task tracking logic
|
||||
- Allows unified search/filter across task types
|
||||
|
||||
**Alternatives Considered**:
|
||||
|
||||
- Separate device task management page (rejected: creates UX fragmentation)
|
||||
- Embed task tracking only in device-import page (rejected: limited visibility)
|
||||
|
||||
**Implementation Notes**:
|
||||
|
||||
- Add task_type field to distinguish ICCID vs Device imports
|
||||
- Task detail page renders different content based on task_type
|
||||
- Device tasks show device_no and bound ICCIDs instead of just ICCID
|
||||
|
||||
### Decision 4: Batch Operation Result Display
|
||||
|
||||
**Choice**: Show detailed results in dialog after batch allocation/recall
|
||||
|
||||
**Rationale**:
|
||||
|
||||
- Operations may partially succeed (some devices succeed, others fail)
|
||||
- Operators need to know exactly which devices failed and why
|
||||
- Allows retry of failed operations without re-selecting all devices
|
||||
|
||||
**Alternatives Considered**:
|
||||
|
||||
- Just show toast notification (rejected: insufficient detail for partial failures)
|
||||
- Navigate to separate results page (rejected: disrupts workflow)
|
||||
|
||||
**Implementation Notes**:
|
||||
|
||||
- Dialog shows summary: total, success count, failure count
|
||||
- Expandable table shows failed devices with error messages
|
||||
- Success closes dialog, partial failure keeps it open for review
|
||||
|
||||
### Decision 5: Component Reuse Strategy
|
||||
|
||||
**Choice**: Use existing Art\* components (ArtTableFullScreen, ArtSearchBar, ArtTable, ArtButtonTable)
|
||||
|
||||
**Rationale**:
|
||||
|
||||
- Maintains UI consistency across the application
|
||||
- Reduces development time
|
||||
- Leverages tested, stable components
|
||||
- Easier for users familiar with other pages
|
||||
|
||||
**Reference Implementation**:
|
||||
|
||||
- Device-list follows role/index.vue pattern
|
||||
- Device-detail follows card-list detail modal pattern
|
||||
- Search form follows enterprise-customer search pattern
|
||||
|
||||
**Implementation Notes**:
|
||||
|
||||
- Use CommonStatus for status values (ENABLED/DISABLED)
|
||||
- Use ElSwitch for status toggles
|
||||
- Use ArtButtonTable for action buttons
|
||||
- Follow ElDescriptions layout for detail pages
|
||||
|
||||
## Architecture Diagrams
|
||||
|
||||
### Device Import Flow
|
||||
|
||||
```
|
||||
┌─────────┐ 1. Select CSV ┌──────────────┐
|
||||
│ Admin │ ──────────────────> │ device-import│
|
||||
│ │ │ (Vue) │
|
||||
└─────────┘ └──────┬───────┘
|
||||
│
|
||||
│ 2. getUploadUrl()
|
||||
v
|
||||
┌──────────────┐
|
||||
│ Storage │
|
||||
│ Service │
|
||||
└──────┬───────┘
|
||||
│
|
||||
│ 3. Upload to OSS
|
||||
v
|
||||
┌──────────────┐
|
||||
│ Object │
|
||||
│ Storage │
|
||||
└──────────────┘
|
||||
│
|
||||
│ 4. importDevices(file_key)
|
||||
v
|
||||
┌──────────────┐
|
||||
│ Device │
|
||||
│ Service │
|
||||
└──────┬───────┘
|
||||
│
|
||||
│ 5. Create Task
|
||||
v
|
||||
┌──────────────┐
|
||||
│ Task │
|
||||
│ Management │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### Device-Card Binding
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────────┐
|
||||
│ Device │ ───── bound to ────> │ Card │
|
||||
│ │ (1 to N) │ │
|
||||
└─────────┘ └──────────────┘
|
||||
│ │
|
||||
│ has_many bindings │ has_one binding
|
||||
v v
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ DeviceCardBinding │
|
||||
│ - device_id │
|
||||
│ - card_id │
|
||||
│ - slot_position (1-4) │
|
||||
│ - bound_at │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Device List Page Load
|
||||
|
||||
1. User navigates to /asset-management/device-list
|
||||
2. Vue component mounts, calls DeviceService.getDevices(params)
|
||||
3. Backend returns paginated device list with bound_card_count
|
||||
4. Table renders with status switches and action buttons
|
||||
|
||||
### Batch Allocation Flow
|
||||
|
||||
1. User selects devices, clicks "Batch Allocate"
|
||||
2. Dialog opens with shop selector
|
||||
3. User selects shop, adds remarks, confirms
|
||||
4. Call DeviceService.allocateDevices({ device_ids, shop_id, remarks })
|
||||
5. Backend processes each device, returns results array
|
||||
6. Dialog shows summary and failed device details
|
||||
|
||||
### Card Binding Flow
|
||||
|
||||
1. User opens device detail page
|
||||
2. Clicks "Bind Card" for an empty slot
|
||||
3. Dialog shows card search and slot selection
|
||||
4. User selects card and slot (e.g., slot 3)
|
||||
5. Call DeviceService.bindCard(device_id, { card_id, slot_position: 3 })
|
||||
6. Backend validates slot is empty, creates binding
|
||||
7. Refresh device detail to show new binding
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Updating Existing Device Import Page
|
||||
|
||||
**Current State** (`src/views/batch/device-import/index.vue`):
|
||||
|
||||
- Uses ElUpload with drag-and-drop
|
||||
- Mock data for import records
|
||||
- No real API integration
|
||||
|
||||
**Migration Steps**:
|
||||
|
||||
1. Replace ElUpload with three-step upload logic
|
||||
- Add getUploadUrl call
|
||||
- Add uploadFile to object storage
|
||||
- Add importDevices call with file_key
|
||||
2. Remove mock import records
|
||||
3. Link to task-management for tracking
|
||||
4. Update CSV format instructions
|
||||
|
||||
**Backward Compatibility**:
|
||||
|
||||
- This is a new feature area with no existing production data
|
||||
- No API breaking changes
|
||||
- UI changes are additive (improve existing page)
|
||||
|
||||
### Extending Task Management
|
||||
|
||||
**Current State**:
|
||||
|
||||
- Only handles ICCID import tasks
|
||||
- Single task type rendering
|
||||
|
||||
**Migration Steps**:
|
||||
|
||||
1. Add task_type filter dropdown (default: show all)
|
||||
2. Add task_type badge in task list
|
||||
3. Task detail page: switch rendering based on task_type
|
||||
4. Add device-specific fields to task detail view
|
||||
|
||||
**Backward Compatibility**:
|
||||
|
||||
- Existing ICCID tasks continue to work unchanged
|
||||
- Filter defaults to showing all types
|
||||
- No database schema changes required (task_type already exists)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Testing
|
||||
|
||||
- DeviceService API methods with mock responses
|
||||
- Form validation logic
|
||||
- Utility functions (formatters, validators)
|
||||
|
||||
### Integration Testing
|
||||
|
||||
- Device list search and pagination
|
||||
- Batch allocation with partial failures
|
||||
- Card binding/unbinding workflow
|
||||
- Import task creation and status tracking
|
||||
|
||||
### E2E Testing Scenarios
|
||||
|
||||
1. Import devices via CSV → verify task created → check task detail
|
||||
2. Search devices → select multiple → allocate to shop → verify shop assignment
|
||||
3. View device detail → bind card to slot 2 → unbind → verify empty slot
|
||||
4. Batch recall devices → verify shop cleared → check operation history
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Device list pagination (default 20 per page)
|
||||
- Debounced search input (300ms delay)
|
||||
- Batch operation result pagination (if >100 results)
|
||||
- Large CSV imports handled asynchronously (no frontend timeout)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Authorization**: All device operations require admin role
|
||||
2. **Input Validation**:
|
||||
- Device number format validation
|
||||
- Slot position range (1 to max_sim_slots)
|
||||
- CSV file size limits
|
||||
3. **Object Storage Security**: Pre-signed URLs expire after 15 minutes
|
||||
4. **Batch Operation Limits**: Backend enforces max 1000 devices per batch
|
||||
5. **XSS Prevention**: All user inputs sanitized before rendering
|
||||
|
||||
## Open Questions
|
||||
|
||||
None at this time. All major design decisions have been made based on existing system patterns and API specifications.
|
||||
@@ -1,67 +0,0 @@
|
||||
# Change: 添加设备管理功能
|
||||
|
||||
## Why
|
||||
|
||||
当前系统只有设备导入页面,缺少完整的设备管理能力。需要提供设备的全生命周期管理,包括:
|
||||
|
||||
- 查看和搜索设备列表
|
||||
- 查看设备详情和绑定的卡信息
|
||||
- 管理设备与卡的绑定关系
|
||||
- 批量分配和回收设备
|
||||
- 完善设备导入流程和任务追踪
|
||||
|
||||
这是物联网卡管理系统的核心资产管理功能之一。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 新增功能
|
||||
|
||||
- **设备列表页面**: 支持多条件搜索、分页、列筛选、批量操作(分配、回收、删除)
|
||||
- **设备详情页面**: 展示设备基本信息、绑定的卡列表、操作历史
|
||||
- **卡绑定管理**: 在设备详情页绑定/解绑卡
|
||||
- **批量分配对话框**: 选择设备批量分配给店铺
|
||||
- **批量回收对话框**: 从店铺批量回收设备
|
||||
- **设备导入改进**: 基于对象存储的三步导入流程
|
||||
- **导入任务管理**: 任务列表和详情查看
|
||||
|
||||
### API 集成
|
||||
|
||||
- DeviceService: 11个API接口
|
||||
- 类型定义: Device, DeviceBinding, ImportTask 等
|
||||
|
||||
### UI 组件
|
||||
|
||||
- 遵循现有组件模式 (ArtTableFullScreen, ArtSearchBar等)
|
||||
- 复用 CommonStatus 统一状态变量
|
||||
- 使用 ElDescriptions、ElTag、ElSwitch 等组件
|
||||
|
||||
## Impact
|
||||
|
||||
### 影响的功能模块
|
||||
|
||||
- **新增**: 资产管理 / 设备列表(主列表页)
|
||||
- **新增**: 资产管理 / 设备详情
|
||||
- **改进**: 批量操作 / 设备导入(改为对象存储模式)
|
||||
- **新增**: 批量操作 / 导入任务列表(独立页面)
|
||||
|
||||
### 影响的代码
|
||||
|
||||
- `src/api/modules/device.ts` (新增)
|
||||
- `src/types/api/device.ts` (新增)
|
||||
- `src/views/asset-management/device-list/index.vue` (新增)
|
||||
- `src/views/asset-management/device-detail/index.vue` (新增)
|
||||
- `src/views/asset-management/task-management/index.vue` (修改:支持设备导入任务)
|
||||
- `src/views/asset-management/task-detail/index.vue` (修改:支持设备导入任务详情)
|
||||
- `src/views/batch/device-import/index.vue` (修改:使用对象存储)
|
||||
- `src/router/routes/asyncRoutes.ts` (新增路由)
|
||||
- `src/router/routesAlias.ts` (新增路由别名)
|
||||
- `src/locales/langs/zh.json` (新增翻译)
|
||||
- `src/api/modules/index.ts` (导出 DeviceService)
|
||||
- `src/types/api/index.ts` (导出 Device 类型)
|
||||
|
||||
### 依赖关系
|
||||
|
||||
- 依赖现有的 StorageService (对象存储)
|
||||
- 依赖现有的 ShopService (店铺选择)
|
||||
- 设备与卡的关联管理
|
||||
- 任务管理页面需要支持多种任务类型(ICCID导入 + 设备导入)
|
||||
@@ -1,159 +0,0 @@
|
||||
# Device Management Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Device List Management
|
||||
|
||||
The system SHALL provide a searchable device list with multi-condition filtering, pagination, and batch operations.
|
||||
|
||||
#### Scenario: Search devices by multiple criteria
|
||||
|
||||
**WHEN** an administrator enters search criteria (device number, device name, status, shop, batch number, device type, manufacturer, creation time range) **THEN** the system shall display only devices matching all specified criteria with pagination support
|
||||
|
||||
#### Scenario: View device list with bound card count
|
||||
|
||||
**WHEN** the device list is displayed **THEN** each device row shall show: ID, device number, device name, device model, device type, manufacturer, max slots, bound card count, status, shop, batch number, activation time, creation time
|
||||
|
||||
#### Scenario: Toggle device status
|
||||
|
||||
**WHEN** an administrator clicks the status switch for a device **THEN** the system shall update the device status between ENABLED and DISABLED and refresh the display
|
||||
|
||||
#### Scenario: Delete a device
|
||||
|
||||
**WHEN** an administrator clicks the delete button and confirms the deletion **THEN** the system shall delete the device and refresh the list
|
||||
|
||||
#### Scenario: Select multiple devices for batch operations
|
||||
|
||||
**WHEN** an administrator checks multiple device rows **THEN** the system shall enable batch operation buttons (Allocate, Recall) and track selected device IDs
|
||||
|
||||
### Requirement: Device Detail Viewing
|
||||
|
||||
The system SHALL display comprehensive device information including basic properties and bound SIM cards.
|
||||
|
||||
#### Scenario: View device basic information
|
||||
|
||||
**WHEN** an administrator navigates to a device detail page **THEN** the system shall display all device properties in a descriptive layout: device number, name, model, type, manufacturer, max SIM slots, status, shop, batch number, activation time, creation time
|
||||
|
||||
#### Scenario: View bound SIM cards with slot positions
|
||||
|
||||
**WHEN** viewing device details **THEN** the system shall display a table of bound cards showing: slot position (1-4), ICCID, phone number, carrier, card status, and binding time
|
||||
|
||||
#### Scenario: Empty slots display
|
||||
|
||||
**WHEN** a device has fewer than max_sim_slots cards bound **THEN** empty slot positions shall be clearly indicated with "Bind Card" action buttons
|
||||
|
||||
### Requirement: Device-Card Binding Management
|
||||
|
||||
The system SHALL allow administrators to bind and unbind SIM cards to specific device slots.
|
||||
|
||||
#### Scenario: Bind a card to a device slot
|
||||
|
||||
**WHEN** an administrator selects a card and slot position (1-4) in the binding dialog **THEN** the system shall create the binding, update the bound card count, and refresh the card list
|
||||
|
||||
#### Scenario: Prevent duplicate slot binding
|
||||
|
||||
**WHEN** an administrator attempts to bind a card to an already occupied slot **THEN** the system shall show an error message and prevent the binding
|
||||
|
||||
#### Scenario: Unbind a card from device
|
||||
|
||||
**WHEN** an administrator clicks unbind for a bound card and confirms **THEN** the system shall remove the binding, decrement the bound card count, and refresh the card list
|
||||
|
||||
#### Scenario: Validate slot range
|
||||
|
||||
**WHEN** an administrator selects a slot position **THEN** the system shall only allow values from 1 to the device's max_sim_slots value
|
||||
|
||||
### Requirement: Batch Device Allocation
|
||||
|
||||
The system SHALL support batch allocation of devices to shops with result tracking.
|
||||
|
||||
#### Scenario: Allocate multiple devices to a shop
|
||||
|
||||
**WHEN** an administrator selects multiple devices, chooses a target shop, adds optional remarks, and confirms allocation **THEN** the system shall allocate all selected devices to the shop and display success/failure results for each device
|
||||
|
||||
#### Scenario: Show allocation results
|
||||
|
||||
**WHEN** the batch allocation completes **THEN** the system shall display: total count, success count, failure count, and detailed failure reasons for each failed device
|
||||
|
||||
#### Scenario: Prevent allocation of already allocated devices
|
||||
|
||||
**WHEN** the batch allocation includes devices already allocated to a shop **THEN** the system shall show warnings for those devices but proceed with allocating unallocated devices
|
||||
|
||||
### Requirement: Batch Device Recall
|
||||
|
||||
The system SHALL support batch recall of devices from shops with result tracking.
|
||||
|
||||
#### Scenario: Recall multiple devices
|
||||
|
||||
**WHEN** an administrator selects multiple devices, adds optional remarks, and confirms recall **THEN** the system shall recall all selected devices from their shops and display success/failure results
|
||||
|
||||
#### Scenario: Show recall results
|
||||
|
||||
**WHEN** the batch recall completes **THEN** the system shall display: total count, success count, failure count, and detailed failure reasons for each failed device
|
||||
|
||||
#### Scenario: Prevent recall of unallocated devices
|
||||
|
||||
**WHEN** the batch recall includes devices not allocated to any shop **THEN** the system shall show warnings for those devices but proceed with recalling allocated devices
|
||||
|
||||
### Requirement: Device Import via Object Storage
|
||||
|
||||
The system SHALL support CSV-based device import using a three-step object storage upload process.
|
||||
|
||||
#### Scenario: Import devices with three-step process
|
||||
|
||||
**WHEN** an administrator uploads a CSV file **THEN** the system shall:
|
||||
|
||||
1. Get upload URL from StorageService.getUploadUrl
|
||||
2. Upload file to object storage using StorageService.uploadFile
|
||||
3. Call DeviceService.importDevices with the file_key **AND** display the task number for tracking
|
||||
|
||||
#### Scenario: Validate CSV format
|
||||
|
||||
**WHEN** the CSV file is uploaded **THEN** the system shall validate the presence of required columns: device_no, device_name, device_model, device_type, max_sim_slots, manufacturer
|
||||
|
||||
#### Scenario: Import devices with card bindings
|
||||
|
||||
**WHEN** the CSV includes iccid_1, iccid_2, iccid_3, iccid_4 columns **THEN** the system shall attempt to bind the specified cards to slots 1-4 respectively during import
|
||||
|
||||
#### Scenario: Handle import errors
|
||||
|
||||
**WHEN** the import process encounters errors (duplicate device_no, invalid ICCID, etc.) **THEN** the system shall record failed rows in the import task with detailed error messages
|
||||
|
||||
### Requirement: Import Task Management
|
||||
|
||||
The system SHALL track device import tasks with detailed status and record information.
|
||||
|
||||
#### Scenario: List import tasks with type filter
|
||||
|
||||
**WHEN** an administrator views the task management page **THEN** the system shall display both ICCID import tasks and Device import tasks with a type filter dropdown
|
||||
|
||||
#### Scenario: View device import task details
|
||||
|
||||
**WHEN** an administrator clicks on a device import task **THEN** the system shall display: task ID, status, total count, success count, failed count, skipped count, created time, completed time
|
||||
|
||||
#### Scenario: View successful import records
|
||||
|
||||
**WHEN** viewing a device import task detail **THEN** the system shall show a table of successfully imported devices with: device_no, device_name, bound ICCIDs
|
||||
|
||||
#### Scenario: View failed import records
|
||||
|
||||
**WHEN** viewing a device import task detail with failures **THEN** the system shall show a table of failed records with: row number, device_no, failure reason
|
||||
|
||||
#### Scenario: Navigate from import page to task detail
|
||||
|
||||
**WHEN** a device import completes successfully **THEN** the system shall display the task number with a link to view task details
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Task Type Support
|
||||
|
||||
Task management SHALL support multiple task types including ICCID import and Device import with type-specific detail views.
|
||||
|
||||
**Previous behavior**: Task management only supported ICCID import tasks.
|
||||
|
||||
#### Scenario: Filter tasks by type
|
||||
|
||||
**WHEN** an administrator selects a task type filter (ICCID Import / Device Import) **THEN** the system shall display only tasks of the selected type
|
||||
|
||||
#### Scenario: Display task type badges
|
||||
|
||||
**WHEN** viewing the task list **THEN** each task shall display a type badge indicating whether it's an ICCID import or Device import task
|
||||
@@ -1,126 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 类型定义和API服务 (Foundation)
|
||||
|
||||
- [ ] 1.1 创建 `src/types/api/device.ts` 定义所有设备相关类型
|
||||
- DeviceItem, DeviceQueryParams, DevicePageResult
|
||||
- DeviceCardBinding, DeviceCardBindingList
|
||||
- AllocateDevicesRequest/Response, RecallDevicesRequest/Response
|
||||
- BindCardRequest/Response, UnbindCardResponse
|
||||
- ImportDeviceRequest/Response, DeviceImportTask相关类型
|
||||
- [ ] 1.2 创建 `src/api/modules/device.ts` 实现 DeviceService
|
||||
- getDevices (列表)
|
||||
- getDeviceById (详情)
|
||||
- deleteDevice (删除)
|
||||
- getDeviceCards (绑定的卡列表)
|
||||
- bindCard (绑定卡)
|
||||
- unbindCard (解绑卡)
|
||||
- allocateDevices (批量分配)
|
||||
- recallDevices (批量回收)
|
||||
- importDevices (批量导入)
|
||||
- getImportTasks (导入任务列表)
|
||||
- getImportTaskDetail (导入任务详情)
|
||||
- [ ] 1.3 在 `src/api/modules/index.ts` 导出 DeviceService
|
||||
- [ ] 1.4 在 `src/types/api/index.ts` 导出 Device 相关类型
|
||||
|
||||
## 2. 设备列表页面 (Core UI)
|
||||
|
||||
- [ ] 2.1 创建 `src/views/asset-management/device-list/index.vue`
|
||||
- 搜索栏:设备号、设备名称、状态、店铺、批次号、设备类型、制造商、创建时间范围
|
||||
- 表格:展示设备信息,支持勾选、列筛选
|
||||
- 表格列:ID、设备号、设备名称、设备型号、设备类型、制造商、最大插槽数、已绑定卡数、状态、店铺、批次号、激活时间、创建时间
|
||||
- 状态使用 CommonStatus + ElSwitch 显示
|
||||
- 操作列:查看详情、删除按钮(使用 ArtButtonTable)
|
||||
- [ ] 2.2 实现批量操作按钮
|
||||
- 批量分配:弹出对话框选择目标店铺
|
||||
- 批量回收:弹出对话框确认回收
|
||||
- 导入设备:跳转到设备导入页面
|
||||
- [ ] 2.3 实现批量分配对话框
|
||||
- 选择目标店铺(下拉搜索)
|
||||
- 显示已选设备数量
|
||||
- 备注输入
|
||||
- 展示分配结果(成功/失败)
|
||||
- [ ] 2.4 实现批量回收对话框
|
||||
- 确认回收设备列表
|
||||
- 备注输入
|
||||
- 展示回收结果(成功/失败)
|
||||
|
||||
## 3. 设备详情页面 (Detail View)
|
||||
|
||||
- [ ] 3.1 创建 `src/views/asset-management/device-detail/index.vue`
|
||||
- 设备基本信息卡片(ElDescriptions)
|
||||
- 绑定的卡列表表格(展示4个插槽位置)
|
||||
- 返回按钮
|
||||
- [ ] 3.2 实现卡绑定管理
|
||||
- 绑定卡按钮:弹出对话框选择卡和插槽位置
|
||||
- 解绑按钮:每个绑定记录旁边
|
||||
- 显示插槽位置(1-4)
|
||||
- 展示卡的基本信息(ICCID、手机号、运营商、状态)
|
||||
|
||||
## 4. 任务管理改进 (Task Management)
|
||||
|
||||
- [ ] 4.1 修改 `src/views/asset-management/task-management/index.vue`
|
||||
- 添加任务类型筛选:ICCID导入 / 设备导入
|
||||
- 支持展示设备导入任务
|
||||
- 任务列表显示任务类型标签
|
||||
- [ ] 4.2 修改 `src/views/asset-management/task-detail/index.vue`
|
||||
- 根据任务类型展示不同的详情内容
|
||||
- 设备导入任务:显示设备号、绑定的ICCID、失败原因
|
||||
- 失败和跳过记录展示
|
||||
|
||||
## 5. 设备导入改进 (Import Enhancement)
|
||||
|
||||
- [ ] 5.1 修改 `src/views/batch/device-import/index.vue`
|
||||
- 改用对象存储三步上传流程
|
||||
1. 获取上传URL (StorageService.getUploadUrl)
|
||||
2. 上传文件到对象存储 (StorageService.uploadFile)
|
||||
3. 调用导入接口 (DeviceService.importDevices)
|
||||
- CSV格式说明:device_no, device_name, device_model, device_type, max_sim_slots, manufacturer, iccid_1~iccid_4, batch_no
|
||||
- 导入成功后显示任务编号,引导去任务管理查看
|
||||
- [ ] 5.2 优化导入记录展示
|
||||
- 移除mock数据,改为真实API调用
|
||||
- 点击"查看详情"跳转到任务详情页
|
||||
|
||||
## 6. 路由和导航 (Routing)
|
||||
|
||||
- [ ] 6.1 在 `src/router/routes/asyncRoutes.ts` 添加路由
|
||||
- /asset-management/device-list (设备列表)
|
||||
- /asset-management/device-detail (设备详情,带query参数id)
|
||||
- [ ] 6.2 在 `src/router/routesAlias.ts` 添加路由别名
|
||||
- DeviceList = '/asset-management/device-list'
|
||||
- DeviceDetail = '/asset-management/device-detail'
|
||||
- [ ] 6.3 在 `src/locales/langs/zh.json` 添加翻译
|
||||
- assetManagement.deviceList: "设备列表"
|
||||
- assetManagement.deviceDetail: "设备详情"
|
||||
|
||||
## 7. 测试和优化 (Testing & Polish)
|
||||
|
||||
- [ ] 7.1 功能测试
|
||||
- 设备列表的搜索、分页、排序
|
||||
- 批量分配和回收流程
|
||||
- 设备与卡的绑定/解绑
|
||||
- 设备导入完整流程
|
||||
- 任务状态查看
|
||||
- [ ] 7.2 边界情况处理
|
||||
- 空列表状态
|
||||
- 加载失败提示
|
||||
- 删除确认提示
|
||||
- 表单验证
|
||||
- [ ] 7.3 性能优化
|
||||
- 列表数据懒加载
|
||||
- 防抖搜索
|
||||
- 批量操作结果分页展示
|
||||
- [ ] 7.4 代码审查
|
||||
- TypeScript类型完整性
|
||||
- 错误处理
|
||||
- 代码复用性
|
||||
- 注释完整性
|
||||
|
||||
## Dependencies
|
||||
|
||||
- 1.x 必须先完成才能开始 2.x-6.x
|
||||
- 2.1-2.2 可以并行开发
|
||||
- 3.x 依赖 1.x,可与 2.x 并行
|
||||
- 4.x 和 5.x 依赖 1.x,可与其他任务并行
|
||||
- 6.x 可在对应页面开发完成后立即进行
|
||||
- 7.x 在所有功能开发完成后进行
|
||||
@@ -1,62 +0,0 @@
|
||||
# Change: Add IoT Card and Device Operations
|
||||
|
||||
## Why
|
||||
|
||||
运营人员和代理商需要更丰富的物联网卡和设备管理功能,包括查询流量使用情况、实名认证状态、卡片状态、启停操作,以及设备的重启、重置、限速、换卡、WiFi设置等操作。这些功能是物联网卡全生命周期管理的核心能力。
|
||||
|
||||
## What Changes
|
||||
|
||||
### IoT Card Operations (6 new APIs)
|
||||
|
||||
- 查询流量使用情况 (GET /api/admin/iot-cards/{iccid}/gateway-flow)
|
||||
- 查询实名认证状态 (GET /api/admin/iot-cards/{iccid}/gateway-realname)
|
||||
- 查询卡片状态 (GET /api/admin/iot-cards/{iccid}/gateway-status)
|
||||
- 获取实名认证链接 (GET /api/admin/iot-cards/{iccid}/realname-link)
|
||||
- 启用物联网卡 (POST /api/admin/iot-cards/{iccid}/start)
|
||||
- 停用物联网卡 (POST /api/admin/iot-cards/{iccid}/stop)
|
||||
|
||||
### Device Operations (6 new APIs)
|
||||
|
||||
- 重启设备 (POST /api/admin/devices/by-imei/{imei}/reboot)
|
||||
- 重置设备 (POST /api/admin/devices/by-imei/{imei}/reset)
|
||||
- 设置限速 (PUT /api/admin/devices/by-imei/{imei}/speed-limit)
|
||||
- 切换SIM卡 (POST /api/admin/devices/by-imei/{imei}/switch-card)
|
||||
- 设置WiFi (PUT /api/admin/devices/by-imei/{imei}/wifi)
|
||||
|
||||
### UI Changes
|
||||
|
||||
- 在 `/asset-management/iot-card-management` 页面添加操作按钮:
|
||||
- "查询流量"按钮(主要操作)
|
||||
- "更多操作"下拉菜单(包含其他5个操作)
|
||||
- 在 `/asset-management/devices` 页面添加操作按钮:
|
||||
- "重启设备"按钮(主要操作)
|
||||
- "更多操作"下拉菜单(包含其他5个操作)
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected Specs
|
||||
|
||||
- **NEW**: `specs/iot-card-operations/spec.md` - IoT卡操作相关的所有需求
|
||||
- **NEW**: `specs/device-operations/spec.md` - 设备操作相关的所有需求
|
||||
|
||||
### Affected Code
|
||||
|
||||
- **API层**:
|
||||
- 新增 `src/api/modules/iotCard.ts` - IoT卡操作API方法
|
||||
- 新增 `src/api/modules/device.ts` - 设备操作API方法
|
||||
- **类型定义**:
|
||||
- 新增 `src/types/api/iotCard.ts` - IoT卡相关类型
|
||||
- 新增 `src/types/api/device.ts` - 设备相关类型
|
||||
- **页面组件**:
|
||||
- 修改 `src/views/asset-management/iot-card-management/index.vue` - 添加操作按钮和对话框
|
||||
- 修改 `src/views/asset-management/devices/index.vue` - 添加操作按钮和对话框
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
无破坏性变更。所有变更都是增量式的新功能添加。
|
||||
|
||||
## Dependencies
|
||||
|
||||
- 后端API已实现(见 `docs/2-3新增接口.md`)
|
||||
- Element Plus UI组件库(已在项目中)
|
||||
- Axios HTTP客户端(已在项目中)
|
||||
@@ -1,194 +0,0 @@
|
||||
# Device Operations Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Reboot Device
|
||||
|
||||
The system SHALL provide the ability to reboot an IoT device via its IMEI.
|
||||
|
||||
#### Scenario: Successfully reboot device
|
||||
|
||||
- **WHEN** an authenticated user initiates a reboot operation on a device with valid IMEI
|
||||
- **THEN** the system sends the reboot command to the device
|
||||
- **AND** returns a success response with operation confirmation
|
||||
|
||||
#### Scenario: Reboot operation with confirmation
|
||||
|
||||
- **WHEN** a user clicks the reboot device button
|
||||
- **THEN** the system prompts for confirmation with a warning message
|
||||
- **AND** only proceeds if the user confirms the action
|
||||
|
||||
#### Scenario: Reboot with invalid IMEI
|
||||
|
||||
- **WHEN** a user attempts to reboot a device with invalid or non-existent IMEI
|
||||
- **THEN** the system returns a 400 error with appropriate error message
|
||||
|
||||
### Requirement: Reset Device to Factory Settings
|
||||
|
||||
The system SHALL provide the ability to reset an IoT device to factory settings via its IMEI.
|
||||
|
||||
#### Scenario: Successfully reset device
|
||||
|
||||
- **WHEN** an authenticated user initiates a factory reset operation on a device
|
||||
- **THEN** the system sends the reset command to the device
|
||||
- **AND** returns a success response
|
||||
|
||||
#### Scenario: Reset operation with strong confirmation
|
||||
|
||||
- **WHEN** a user clicks the factory reset button
|
||||
- **THEN** the system displays a warning dialog explaining data loss
|
||||
- **AND** requires user confirmation before proceeding
|
||||
- **AND** only executes if the user explicitly confirms
|
||||
|
||||
#### Scenario: Insufficient permissions for reset
|
||||
|
||||
- **WHEN** a user without proper permissions attempts to reset a device
|
||||
- **THEN** the system returns a 403 forbidden error
|
||||
|
||||
### Requirement: Set Device Speed Limit
|
||||
|
||||
The system SHALL provide the ability to configure upload and download speed limits for an IoT device.
|
||||
|
||||
#### Scenario: Successfully set speed limit
|
||||
|
||||
- **WHEN** an authenticated user submits valid speed limit parameters
|
||||
- **THEN** the system applies the upload and download speed limits to the device
|
||||
- **AND** returns a success response
|
||||
|
||||
#### Scenario: Configure speed limits via dialog
|
||||
|
||||
- **WHEN** a user selects the speed limit option
|
||||
- **THEN** the system displays a dialog with input fields for upload_speed and download_speed (KB/s)
|
||||
- **AND** validates that both values are integers >= 1
|
||||
|
||||
#### Scenario: Invalid speed limit parameters
|
||||
|
||||
- **WHEN** a user submits speed limits less than 1 KB/s
|
||||
- **THEN** the system returns a 400 parameter error
|
||||
- **AND** displays validation error messages
|
||||
|
||||
### Requirement: Switch Device SIM Card
|
||||
|
||||
The system SHALL provide the ability to switch the active SIM card on a multi-SIM device.
|
||||
|
||||
#### Scenario: Successfully switch to target card
|
||||
|
||||
- **WHEN** an authenticated user initiates a card switch with valid target ICCID
|
||||
- **THEN** the system switches the device to use the specified card
|
||||
- **AND** returns a success response
|
||||
|
||||
#### Scenario: Switch card via dialog
|
||||
|
||||
- **WHEN** a user selects the switch card option
|
||||
- **THEN** the system displays a dialog prompting for target_iccid
|
||||
- **AND** validates the ICCID format
|
||||
|
||||
#### Scenario: Switch to non-existent card
|
||||
|
||||
- **WHEN** a user attempts to switch to an ICCID that doesn't exist or isn't available
|
||||
- **THEN** the system returns a 400 error with descriptive message
|
||||
|
||||
### Requirement: Configure Device WiFi Settings
|
||||
|
||||
The system SHALL provide the ability to configure WiFi settings including SSID, password, and enabled status.
|
||||
|
||||
#### Scenario: Successfully configure WiFi
|
||||
|
||||
- **WHEN** an authenticated user submits valid WiFi configuration
|
||||
- **THEN** the system applies the WiFi settings to the device
|
||||
- **AND** returns a success response
|
||||
|
||||
#### Scenario: Configure WiFi via dialog
|
||||
|
||||
- **WHEN** a user selects the WiFi configuration option
|
||||
- **THEN** the system displays a dialog with fields for:
|
||||
- enabled (启用状态: 0=禁用, 1=启用)
|
||||
- ssid (WiFi名称, 1-32 characters)
|
||||
- password (WiFi密码, 8-63 characters)
|
||||
- **AND** validates all field constraints
|
||||
|
||||
#### Scenario: Invalid WiFi parameters
|
||||
|
||||
- **WHEN** a user submits WiFi configuration with invalid parameters
|
||||
- **THEN** the system returns a 400 error
|
||||
- **AND** displays specific validation errors (e.g., "SSID too long", "Password too short")
|
||||
|
||||
### Requirement: Device Management UI Integration
|
||||
|
||||
The system SHALL integrate device operations into the device management page at `/asset-management/devices`.
|
||||
|
||||
#### Scenario: Display primary operation button
|
||||
|
||||
- **WHEN** a user views the device management page
|
||||
- **THEN** the system displays a "重启设备" (Reboot Device) button as the primary operation
|
||||
|
||||
#### Scenario: Display dropdown menu for additional operations
|
||||
|
||||
- **WHEN** a user views the device management page
|
||||
- **THEN** the system displays a "更多操作" (More Operations) dropdown menu
|
||||
- **AND** the dropdown contains: 恢复出厂, 设置限速, 切换SIM卡, 设置WiFi
|
||||
|
||||
#### Scenario: Show loading indicator during operations
|
||||
|
||||
- **WHEN** a device operation is in progress
|
||||
- **THEN** the system displays a loading indicator
|
||||
- **AND** disables the operation buttons to prevent duplicate requests
|
||||
|
||||
### Requirement: Authentication and Authorization
|
||||
|
||||
The system SHALL enforce JWT-based authentication for all device operations.
|
||||
|
||||
#### Scenario: Access with valid JWT token
|
||||
|
||||
- **WHEN** a user makes a request with a valid Bearer token
|
||||
- **THEN** the system processes the request normally
|
||||
|
||||
#### Scenario: Access with expired token
|
||||
|
||||
- **WHEN** a user makes a request with an expired JWT token
|
||||
- **THEN** the system returns a 401 unauthorized error
|
||||
- **AND** redirects to the login page
|
||||
|
||||
### Requirement: Error Handling and User Feedback
|
||||
|
||||
The system SHALL provide clear error messages and success notifications for all operations.
|
||||
|
||||
#### Scenario: Display success message
|
||||
|
||||
- **WHEN** a device operation completes successfully
|
||||
- **THEN** the system displays a success message notification
|
||||
- **AND** automatically closes the operation dialog
|
||||
|
||||
#### Scenario: Handle network errors
|
||||
|
||||
- **WHEN** a network error occurs during a device operation
|
||||
- **THEN** the system displays a user-friendly error message
|
||||
- **AND** allows the user to retry the operation
|
||||
|
||||
#### Scenario: Handle server errors
|
||||
|
||||
- **WHEN** a 500 server error occurs
|
||||
- **THEN** the system displays an error message with timestamp
|
||||
- **AND** logs the error for debugging
|
||||
|
||||
### Requirement: Operation Confirmation Dialogs
|
||||
|
||||
The system SHALL require user confirmation for destructive operations.
|
||||
|
||||
#### Scenario: Confirm reboot operation
|
||||
|
||||
- **WHEN** a user initiates a reboot
|
||||
- **THEN** the system shows a confirmation dialog stating "确定要重启该设备吗?"
|
||||
- **AND** requires explicit confirmation
|
||||
|
||||
#### Scenario: Confirm factory reset operation
|
||||
|
||||
- **WHEN** a user initiates a factory reset
|
||||
- **THEN** the system shows a strong warning dialog stating "确定要恢复出厂设置吗?此操作将清除所有数据!"
|
||||
- **AND** requires explicit confirmation
|
||||
|
||||
#### Scenario: No confirmation for query operations
|
||||
|
||||
- **WHEN** a user initiates speed limit, card switch, or WiFi configuration
|
||||
- **THEN** the system displays input dialogs without confirmation prompts
|
||||
- **AND** only executes after user submits valid parameters
|
||||
@@ -1,151 +0,0 @@
|
||||
# IoT Card Operations Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Query IoT Card Flow Usage
|
||||
|
||||
The system SHALL provide the ability to query real-time flow usage for an IoT card via its ICCID.
|
||||
|
||||
#### Scenario: Successfully query flow usage
|
||||
|
||||
- **WHEN** an authenticated user requests flow usage for a valid ICCID
|
||||
- **THEN** the system returns flow usage data including used flow amount and unit
|
||||
|
||||
#### Scenario: Query with invalid ICCID
|
||||
|
||||
- **WHEN** a user requests flow usage for an invalid or non-existent ICCID
|
||||
- **THEN** the system returns a 400 error with appropriate error message
|
||||
|
||||
### Requirement: Query IoT Card Realname Status
|
||||
|
||||
The system SHALL provide the ability to query the realname authentication status for an IoT card via its ICCID.
|
||||
|
||||
#### Scenario: Successfully query realname status
|
||||
|
||||
- **WHEN** an authenticated user requests realname status for a valid ICCID
|
||||
- **THEN** the system returns the current realname authentication status
|
||||
|
||||
#### Scenario: Query status for unauthenticated user
|
||||
|
||||
- **WHEN** an unauthenticated user attempts to query realname status
|
||||
- **THEN** the system returns a 401 unauthorized error
|
||||
|
||||
### Requirement: Query IoT Card Real-time Status
|
||||
|
||||
The system SHALL provide the ability to query the real-time operational status of an IoT card via its ICCID.
|
||||
|
||||
#### Scenario: Successfully query card status
|
||||
|
||||
- **WHEN** an authenticated user requests card status for a valid ICCID
|
||||
- **THEN** the system returns card status information including ICCID and current status (准备/正常/停机)
|
||||
|
||||
#### Scenario: Query with unauthorized access
|
||||
|
||||
- **WHEN** a user without proper permissions attempts to query card status
|
||||
- **THEN** the system returns a 403 forbidden error
|
||||
|
||||
### Requirement: Get IoT Card Realname Link
|
||||
|
||||
The system SHALL provide the ability to generate and retrieve a realname authentication link for an IoT card.
|
||||
|
||||
#### Scenario: Successfully retrieve realname link
|
||||
|
||||
- **WHEN** an authenticated user requests the realname link for a valid ICCID
|
||||
- **THEN** the system returns an HTTPS URL that can be used for realname authentication
|
||||
|
||||
#### Scenario: Display realname link as QR code
|
||||
|
||||
- **WHEN** the system returns a realname authentication link
|
||||
- **THEN** the UI displays the link as a QR code for easy mobile scanning
|
||||
|
||||
### Requirement: Start IoT Card (复机)
|
||||
|
||||
The system SHALL provide the ability to start (restore service) an IoT card via its ICCID.
|
||||
|
||||
#### Scenario: Successfully start a stopped card
|
||||
|
||||
- **WHEN** an authenticated user initiates a start operation on a stopped card
|
||||
- **THEN** the system processes the request and restores card service
|
||||
- **AND** displays a success message to the user
|
||||
|
||||
#### Scenario: Start operation with confirmation
|
||||
|
||||
- **WHEN** a user clicks the start card button
|
||||
- **THEN** the system prompts for confirmation before executing the operation
|
||||
- **AND** only proceeds if the user confirms the action
|
||||
|
||||
#### Scenario: Insufficient permissions for start operation
|
||||
|
||||
- **WHEN** a user without proper permissions attempts to start a card
|
||||
- **THEN** the system returns a 403 forbidden error
|
||||
|
||||
### Requirement: Stop IoT Card (停机)
|
||||
|
||||
The system SHALL provide the ability to stop (suspend service) an IoT card via its ICCID.
|
||||
|
||||
#### Scenario: Successfully stop an active card
|
||||
|
||||
- **WHEN** an authenticated user initiates a stop operation on an active card
|
||||
- **THEN** the system processes the request and suspends card service
|
||||
- **AND** displays a success message to the user
|
||||
|
||||
#### Scenario: Stop operation with confirmation
|
||||
|
||||
- **WHEN** a user clicks the stop card button
|
||||
- **THEN** the system prompts for confirmation before executing the operation
|
||||
- **AND** only proceeds if the user confirms the action
|
||||
|
||||
#### Scenario: Server error during stop operation
|
||||
|
||||
- **WHEN** a server error occurs during the stop operation
|
||||
- **THEN** the system returns a 500 error with error details
|
||||
- **AND** displays an appropriate error message to the user
|
||||
|
||||
### Requirement: IoT Card Management UI Integration
|
||||
|
||||
The system SHALL integrate IoT card operations into the card management page at `/asset-management/iot-card-management`.
|
||||
|
||||
#### Scenario: Display primary operation button
|
||||
|
||||
- **WHEN** a user views the IoT card management page
|
||||
- **THEN** the system displays a "查询流量使用" (Query Flow Usage) button as the primary operation
|
||||
|
||||
#### Scenario: Display dropdown menu for additional operations
|
||||
|
||||
- **WHEN** a user views the IoT card management page
|
||||
- **THEN** the system displays a "更多操作" (More Operations) dropdown menu
|
||||
- **AND** the dropdown contains: 查询实名状态, 查询卡状态, 获取实名链接, 启用卡片, 停用卡片
|
||||
|
||||
#### Scenario: Show operation results in dialog
|
||||
|
||||
- **WHEN** a user executes a query operation (flow, realname status, or card status)
|
||||
- **THEN** the system displays results in a modal dialog with properly formatted data
|
||||
|
||||
### Requirement: Authentication and Authorization
|
||||
|
||||
The system SHALL enforce JWT-based authentication for all IoT card operations.
|
||||
|
||||
#### Scenario: Access with valid JWT token
|
||||
|
||||
- **WHEN** a user makes a request with a valid Bearer token
|
||||
- **THEN** the system processes the request normally
|
||||
|
||||
#### Scenario: Access with expired token
|
||||
|
||||
- **WHEN** a user makes a request with an expired JWT token
|
||||
- **THEN** the system returns a 401 unauthorized error
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
The system SHALL provide clear error messages for all failure scenarios.
|
||||
|
||||
#### Scenario: Handle 400 parameter errors
|
||||
|
||||
- **WHEN** a request contains invalid parameters
|
||||
- **THEN** the system returns a 400 error with specific validation failure details
|
||||
|
||||
#### Scenario: Handle 500 server errors
|
||||
|
||||
- **WHEN** an internal server error occurs
|
||||
- **THEN** the system returns a 500 error with error timestamp
|
||||
- **AND** logs the error for debugging purposes
|
||||
@@ -1,86 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. API Layer - IoT Card Operations
|
||||
|
||||
- [ ] 1.1 创建 `src/api/modules/iotCard.ts`
|
||||
- [ ] 1.2 实现 `getGatewayFlow(iccid)` - 查询流量使用
|
||||
- [ ] 1.3 实现 `getGatewayRealname(iccid)` - 查询实名状态
|
||||
- [ ] 1.4 实现 `getGatewayStatus(iccid)` - 查询卡片状态
|
||||
- [ ] 1.5 实现 `getRealnameLink(iccid)` - 获取实名链接
|
||||
- [ ] 1.6 实现 `startCard(iccid)` - 启用卡片
|
||||
- [ ] 1.7 实现 `stopCard(iccid)` - 停用卡片
|
||||
|
||||
## 2. API Layer - Device Operations
|
||||
|
||||
- [ ] 2.1 创建 `src/api/modules/device.ts`
|
||||
- [ ] 2.2 实现 `rebootDevice(imei)` - 重启设备
|
||||
- [ ] 2.3 实现 `resetDevice(imei)` - 重置设备
|
||||
- [ ] 2.4 实现 `setSpeedLimit(imei, params)` - 设置限速
|
||||
- [ ] 2.5 实现 `switchCard(imei, params)` - 切换SIM卡
|
||||
- [ ] 2.6 实现 `setWifi(imei, params)` - 设置WiFi
|
||||
|
||||
## 3. Type Definitions - IoT Card
|
||||
|
||||
- [ ] 3.1 创建 `src/types/api/iotCard.ts`
|
||||
- [ ] 3.2 定义流量使用响应类型 `GatewayFlowResponse`
|
||||
- [ ] 3.3 定义实名状态响应类型 `GatewayRealnameResponse`
|
||||
- [ ] 3.4 定义卡片状态响应类型 `GatewayStatusResponse`
|
||||
- [ ] 3.5 定义实名链接响应类型 `RealnameUrlResponse`
|
||||
- [ ] 3.6 定义启停操作请求类型 `StartStopCardRequest`
|
||||
|
||||
## 4. Type Definitions - Device
|
||||
|
||||
- [ ] 4.1 创建 `src/types/api/device.ts`
|
||||
- [ ] 4.2 定义限速参数类型 `SpeedLimitParams`
|
||||
- [ ] 4.3 定义换卡参数类型 `SwitchCardParams`
|
||||
- [ ] 4.4 定义WiFi参数类型 `WifiParams`
|
||||
- [ ] 4.5 定义操作响应类型 `DeviceOperationResponse`
|
||||
|
||||
## 5. UI - IoT Card Management Page
|
||||
|
||||
- [ ] 5.1 在表格操作列添加"查询流量"按钮
|
||||
- [ ] 5.2 在表格操作列添加"更多操作"下拉菜单
|
||||
- [ ] 5.3 创建"流量使用查询"对话框组件
|
||||
- [ ] 5.4 创建"实名状态查询"对话框组件
|
||||
- [ ] 5.5 创建"卡片状态查询"对话框组件
|
||||
- [ ] 5.6 创建"获取实名链接"对话框组件(显示二维码)
|
||||
- [ ] 5.7 实现"启用卡片"操作(带确认提示)
|
||||
- [ ] 5.8 实现"停用卡片"操作(带确认提示)
|
||||
|
||||
## 6. UI - Device Management Page
|
||||
|
||||
- [ ] 6.1 在表格操作列添加"重启设备"按钮
|
||||
- [ ] 6.2 在表格操作列添加"更多操作"下拉菜单
|
||||
- [ ] 6.3 实现"重启设备"操作(带确认提示)
|
||||
- [ ] 6.4 实现"重置设备"操作(带确认提示)
|
||||
- [ ] 6.5 创建"设置限速"对话框组件(包含上下行速率输入)
|
||||
- [ ] 6.6 创建"切换SIM卡"对话框组件(选择卡槽)
|
||||
- [ ] 6.7 创建"设置WiFi"对话框组件(SSID、密码、频段等)
|
||||
|
||||
## 7. Error Handling & User Feedback
|
||||
|
||||
- [ ] 7.1 为所有API调用添加错误处理
|
||||
- [ ] 7.2 添加操作成功提示消息
|
||||
- [ ] 7.3 添加操作失败错误提示
|
||||
- [ ] 7.4 添加加载状态指示器
|
||||
|
||||
## 8. Permission Control
|
||||
|
||||
- [ ] 8.1 检查IoT卡操作权限配置
|
||||
- [ ] 8.2 检查设备操作权限配置
|
||||
- [ ] 8.3 根据权限显示/隐藏操作按钮
|
||||
|
||||
## 9. Testing & Validation
|
||||
|
||||
- [ ] 9.1 测试所有IoT卡操作API调用
|
||||
- [ ] 9.2 测试所有设备操作API调用
|
||||
- [ ] 9.3 测试UI交互和对话框显示
|
||||
- [ ] 9.4 测试错误处理场景
|
||||
- [ ] 9.5 测试权限控制
|
||||
- [ ] 9.6 验证响应数据格式和显示
|
||||
|
||||
## 10. Documentation
|
||||
|
||||
- [ ] 10.1 更新API文档(如有需要)
|
||||
- [ ] 10.2 添加操作说明注释
|
||||
- [ ] 10.3 更新用户手册(如有需要)
|
||||
@@ -1,33 +0,0 @@
|
||||
# Change: 重命名 API 字段 series_allocation_id 为 series_id
|
||||
|
||||
## Why
|
||||
|
||||
后端 API 字段命名不一致。前端表单使用 `series_id`(套餐系列ID),但 API 参数仍使用 `series_allocation_id`(套餐系列分配ID)。这导致命名混淆且不符合实际业务含义(现在直接指向套餐系列 ID,而非分配记录 ID)。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: 重命名 4 个 API 端点的请求/响应字段
|
||||
|
||||
- `PATCH /api/admin/iot-cards/series-binding` - 请求参数 `series_allocation_id` → `series_id`
|
||||
- `PATCH /api/admin/devices/series-binding` - 请求参数 `series_allocation_id` → `series_id`
|
||||
- `GET /api/admin/iot-cards/standalone` - 查询参数和响应字段 `series_allocation_id` → `series_id`
|
||||
- `GET /api/admin/devices` - 查询参数和响应字段 `series_allocation_id` → `series_id`
|
||||
|
||||
- 前端同步修改:
|
||||
- TypeScript 类型定义
|
||||
- API 方法参数
|
||||
- 页面组件调用代码
|
||||
- 移除临时注释
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected specs**: iot-card-api, device-api
|
||||
- **Affected code**:
|
||||
- `src/types/api/device.ts` (BatchSetDeviceSeriesBindingRequest)
|
||||
- `src/types/api/card.ts` (BatchSetCardSeriesBindingRequest)
|
||||
- `src/api/modules/device.ts` (batchSetDeviceSeriesBinding)
|
||||
- `src/api/modules/card.ts` (batchSetCardSeriesBinding)
|
||||
- `src/views/asset-management/device-list/index.vue`
|
||||
- `src/views/asset-management/iot-card-management/index.vue`
|
||||
- **Breaking change**: 需要后端先部署更新,前端再部署
|
||||
- **Migration**: 更新所有使用这些字段的 API 调用
|
||||
@@ -1,104 +0,0 @@
|
||||
# Device API Specification Delta
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Device Series ID Query Parameter
|
||||
|
||||
The system SHALL support filtering devices by package series ID in query parameters.
|
||||
|
||||
**Query Parameter**: `series_id` (number, optional)
|
||||
|
||||
#### Scenario: Filter devices by series
|
||||
|
||||
- **WHEN** admin queries devices with series_id parameter
|
||||
- **THEN** system returns only devices associated with that package series
|
||||
- **AND** returns empty list if no matches found
|
||||
|
||||
#### Scenario: Query without series filter
|
||||
|
||||
- **WHEN** admin queries devices without series_id parameter
|
||||
- **THEN** system returns all devices matching other filter criteria
|
||||
- **AND** series association is not considered
|
||||
|
||||
### Requirement: Device Series ID Response Field
|
||||
|
||||
The system SHALL include package series ID in device response data.
|
||||
|
||||
**Response Field**: `series_id` (number | null)
|
||||
|
||||
- Value is package series ID if device is bound to a series
|
||||
- Value is null if device has no series binding
|
||||
|
||||
#### Scenario: Display series association
|
||||
|
||||
- **WHEN** system returns device in list response
|
||||
- **THEN** each device includes series_id field
|
||||
- **AND** field shows current series binding or null
|
||||
|
||||
#### Scenario: Null series binding
|
||||
|
||||
- **WHEN** device has no series binding
|
||||
- **THEN** series_id field is null
|
||||
- **AND** device can still be displayed in results
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
### API Field Renaming
|
||||
|
||||
- FROM: `series_allocation_id` (in batch series binding endpoints)
|
||||
- TO: `series_id`
|
||||
|
||||
**Reason**: Field name `series_allocation_id` implied a reference to an allocation record ID, but it actually stores the package series ID directly. Renaming to `series_id` clarifies this relationship and aligns with frontend naming conventions.
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Batch Set Device Series Binding
|
||||
|
||||
The system SHALL provide an endpoint to batch set package series binding for devices.
|
||||
|
||||
**Endpoint**: `PATCH /api/admin/devices/series-binding`
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"device_ids": [1, 2, 3],
|
||||
"series_id": 123 // Changed from series_allocation_id
|
||||
}
|
||||
```
|
||||
|
||||
**Field Definitions**:
|
||||
|
||||
- `device_ids`: Array of device IDs (max 500 items)
|
||||
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"success_count": 3,
|
||||
"fail_count": 0,
|
||||
"failed_items": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: Successful batch series binding
|
||||
|
||||
- **WHEN** admin calls batch series binding API with valid device IDs and series_id
|
||||
- **THEN** system updates series association for all valid devices
|
||||
- **AND** returns success count and failure details
|
||||
|
||||
#### Scenario: Clear series association
|
||||
|
||||
- **WHEN** admin calls batch series binding API with series_id = 0
|
||||
- **THEN** system clears series association for specified devices
|
||||
- **AND** returns success confirmation
|
||||
|
||||
#### Scenario: Partial failure handling
|
||||
|
||||
- **WHEN** some devices in the batch cannot be updated
|
||||
- **THEN** system processes valid devices successfully
|
||||
- **AND** returns failed_items list with reasons for failures
|
||||
@@ -1,104 +0,0 @@
|
||||
# IoT Card API Specification Delta
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: IoT Card Series ID Query Parameter
|
||||
|
||||
The system SHALL support filtering standalone IoT cards by package series ID in query parameters.
|
||||
|
||||
**Query Parameter**: `series_id` (number, optional)
|
||||
|
||||
#### Scenario: Filter cards by series
|
||||
|
||||
- **WHEN** admin queries standalone cards with series_id parameter
|
||||
- **THEN** system returns only cards associated with that package series
|
||||
- **AND** returns empty list if no matches found
|
||||
|
||||
#### Scenario: Query without series filter
|
||||
|
||||
- **WHEN** admin queries cards without series_id parameter
|
||||
- **THEN** system returns all cards matching other filter criteria
|
||||
- **AND** series association is not considered
|
||||
|
||||
### Requirement: IoT Card Series ID Response Field
|
||||
|
||||
The system SHALL include package series ID in standalone IoT card response data.
|
||||
|
||||
**Response Field**: `series_id` (number | null)
|
||||
|
||||
- Value is package series ID if card is bound to a series
|
||||
- Value is null if card has no series binding
|
||||
|
||||
#### Scenario: Display series association
|
||||
|
||||
- **WHEN** system returns IoT card in list response
|
||||
- **THEN** each card includes series_id field
|
||||
- **AND** field shows current series binding or null
|
||||
|
||||
#### Scenario: Null series binding
|
||||
|
||||
- **WHEN** card has no series binding
|
||||
- **THEN** series_id field is null
|
||||
- **AND** card can still be displayed in results
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
### API Field Renaming
|
||||
|
||||
- FROM: `series_allocation_id` (in batch series binding endpoints)
|
||||
- TO: `series_id`
|
||||
|
||||
**Reason**: Field name `series_allocation_id` implied a reference to an allocation record ID, but it actually stores the package series ID directly. Renaming to `series_id` clarifies this relationship and aligns with frontend naming conventions.
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Batch Set Card Series Binding
|
||||
|
||||
The system SHALL provide an endpoint to batch set package series binding for IoT cards.
|
||||
|
||||
**Endpoint**: `PATCH /api/admin/iot-cards/series-binding`
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"iccids": ["898600..."],
|
||||
"series_id": 123 // Changed from series_allocation_id
|
||||
}
|
||||
```
|
||||
|
||||
**Field Definitions**:
|
||||
|
||||
- `iccids`: Array of ICCIDs (max 500 items)
|
||||
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"success_count": 100,
|
||||
"fail_count": 0,
|
||||
"failed_items": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: Successful batch series binding
|
||||
|
||||
- **WHEN** admin calls batch series binding API with valid ICCIDs and series_id
|
||||
- **THEN** system updates series association for all valid cards
|
||||
- **AND** returns success count and failure details
|
||||
|
||||
#### Scenario: Clear series association
|
||||
|
||||
- **WHEN** admin calls batch series binding API with series_id = 0
|
||||
- **THEN** system clears series association for specified cards
|
||||
- **AND** returns success confirmation
|
||||
|
||||
#### Scenario: Partial failure handling
|
||||
|
||||
- **WHEN** some cards in the batch cannot be updated
|
||||
- **THEN** system processes valid cards successfully
|
||||
- **AND** returns failed_items list with reasons for failures
|
||||
@@ -1,37 +0,0 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. 类型定义更新
|
||||
|
||||
### 1.1 批量设置接口参数重命名
|
||||
|
||||
- [x] 1.1.1 更新 `src/types/api/device.ts` 中 BatchSetDeviceSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
|
||||
- [x] 1.1.2 更新 `src/types/api/card.ts` 中 BatchSetCardSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
|
||||
|
||||
### 1.2 查询参数新增字段
|
||||
|
||||
- [x] 1.2.1 在 `src/types/api/device.ts` 的 DeviceQueryParams 接口中添加 `series_id?: number` 查询参数
|
||||
- [x] 1.2.2 在 `src/types/api/card.ts` 的 StandaloneCardQueryParams 接口中添加 `series_id?: number` 查询参数
|
||||
|
||||
### 1.3 响应类型新增字段
|
||||
|
||||
- [x] 1.3.1 在 `src/types/api/device.ts` 的 Device 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
|
||||
- [x] 1.3.2 在 `src/types/api/card.ts` 的 StandaloneIotCard 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
|
||||
|
||||
## 2. API 方法更新
|
||||
|
||||
- [x] 2.1 更新 `src/api/modules/device.ts` 中 batchSetDeviceSeriesBinding 方法参数,将 `series_allocation_id` 改为 `series_id`
|
||||
- [x] 2.2 更新 `src/api/modules/card.ts` 中 batchSetCardSeriesBinding 方法参数,将 `series_allocation_id` 改为 `series_id`
|
||||
|
||||
## 3. 页面组件更新
|
||||
|
||||
- [x] 3.1 更新 `src/views/asset-management/device-list/index.vue` 中调用 batchSetDeviceSeriesBinding 的代码,将参数名从 `series_allocation_id` 改为 `series_id`
|
||||
- [x] 3.2 移除 device-list/index.vue 中第 1191 行的临时注释
|
||||
- [x] 3.3 更新 `src/views/asset-management/iot-card-management/index.vue` 中调用 batchSetCardSeriesBinding 的代码,将参数名从 `series_allocation_id` 改为 `series_id`
|
||||
- [x] 3.4 移除 iot-card-management/index.vue 中第 1326 行的临时注释
|
||||
|
||||
## 4. 验证
|
||||
|
||||
- [x] 4.1 运行 TypeScript 类型检查确认无类型错误
|
||||
- [ ] 4.2 本地测试批量设置 IoT 卡系列绑定功能
|
||||
- [ ] 4.3 本地测试批量设置设备系列绑定功能
|
||||
- [ ] 4.4 确认所有相关功能正常工作
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -88,13 +88,6 @@
|
||||
iconColor: '#ff3b30',
|
||||
path: RoutesAlias.Analysis
|
||||
},
|
||||
{
|
||||
name: '礼花效果',
|
||||
description: '动画特效展示',
|
||||
icon: '',
|
||||
iconColor: '#7A7FFF',
|
||||
path: RoutesAlias.Fireworks
|
||||
},
|
||||
{
|
||||
name: '聊天',
|
||||
description: '即时通讯功能',
|
||||
@@ -116,13 +109,6 @@
|
||||
iconColor: '#ff6b6b',
|
||||
path: WEB_LINKS.COMMUNITY
|
||||
},
|
||||
{
|
||||
name: '更新日志',
|
||||
description: '版本更新与变更记录',
|
||||
icon: '',
|
||||
iconColor: '#38C0FC',
|
||||
path: RoutesAlias.ChangeLog
|
||||
},
|
||||
{
|
||||
name: '哔哩哔哩',
|
||||
description: '技术分享与交流',
|
||||
@@ -134,11 +120,7 @@
|
||||
|
||||
const quickLinks: QuickLink[] = [
|
||||
{ name: '登录', path: RoutesAlias.Login },
|
||||
{ name: '注册', path: RoutesAlias.Register },
|
||||
{ name: '忘记密码', path: RoutesAlias.ForgetPassword },
|
||||
{ name: '定价', path: RoutesAlias.Pricing },
|
||||
{ name: '个人中心', path: RoutesAlias.UserCenter },
|
||||
{ name: '留言管理', path: RoutesAlias.Comment }
|
||||
{ name: '个人中心', path: RoutesAlias.UserCenter }
|
||||
]
|
||||
|
||||
const handleAppClick = (path: string) => {
|
||||
|
||||
@@ -362,6 +362,15 @@
|
||||
|
||||
const actionList = props.actions(row)
|
||||
|
||||
// 如果没有操作按钮,显示提示
|
||||
if (actionList.length === 0) {
|
||||
return h(
|
||||
'span',
|
||||
{ style: 'color: var(--el-text-color-placeholder); font-size: 12px;' },
|
||||
'暂无可用操作'
|
||||
)
|
||||
}
|
||||
|
||||
// 如果操作数量 <= 2,直接显示所有按钮
|
||||
if (actionList.length <= 2) {
|
||||
return h(
|
||||
|
||||
@@ -312,38 +312,17 @@
|
||||
"ecommerce": "电子商务"
|
||||
},
|
||||
"widgets": {
|
||||
"title": "组件中心",
|
||||
"iconList": "Icon 图标",
|
||||
"iconSelector": "图标选择器",
|
||||
"imageCrop": "图像裁剪",
|
||||
"excel": "Excel 导入导出",
|
||||
"video": "视频播放器",
|
||||
"countTo": "数字滚动",
|
||||
"wangEditor": "富文本编辑器",
|
||||
"watermark": "水印",
|
||||
"contextMenu": "右键菜单",
|
||||
"qrcode": "二维码",
|
||||
"drag": "拖拽",
|
||||
"textScroll": "文字滚动",
|
||||
"fireworks": "礼花",
|
||||
"elementUI": "组件总览"
|
||||
"drag": "拖拽"
|
||||
},
|
||||
"template": {
|
||||
"title": "模板中心",
|
||||
"chat": "聊天",
|
||||
"cards": "卡片",
|
||||
"banners": "横幅",
|
||||
"charts": "图表",
|
||||
"map": "地图",
|
||||
"calendar": "日历",
|
||||
"pricing": "定价"
|
||||
},
|
||||
"article": {
|
||||
"title": "文章管理",
|
||||
"articleList": "文章列表",
|
||||
"articleDetail": "文章详情",
|
||||
"comment": "留言管理",
|
||||
"articlePublish": "文章发布"
|
||||
"calendar": "日历"
|
||||
},
|
||||
"result": {
|
||||
"title": "结果页面",
|
||||
@@ -361,55 +340,16 @@
|
||||
"role": "角色管理",
|
||||
"permission": "权限管理",
|
||||
"carrierManagement": "运营商管理",
|
||||
"userCenter": "个人中心",
|
||||
"menu": "菜单管理",
|
||||
"nested": "嵌套菜单",
|
||||
"menu1": "菜单1",
|
||||
"menu2": "菜单2",
|
||||
"menu21": "菜单2-1",
|
||||
"menu3": "菜单3",
|
||||
"menu31": "菜单3-1",
|
||||
"menu32": "菜单3-2",
|
||||
"menu321": "菜单3-2-1"
|
||||
},
|
||||
"safeguard": {
|
||||
"title": "运维管理",
|
||||
"server": "服务器管理"
|
||||
},
|
||||
"plan": {
|
||||
"title": "版本计划",
|
||||
"log": "更新日志"
|
||||
},
|
||||
"help": {
|
||||
"title": "帮助中心",
|
||||
"document": "官方文档"
|
||||
},
|
||||
"cardManagement": {
|
||||
"title": "我的网卡",
|
||||
"singleCard": "资产信息",
|
||||
"cardList": "网卡管理",
|
||||
"cardDetail": "网卡明细",
|
||||
"cardAssign": "网卡分配",
|
||||
"cardShutdown": "停机管理",
|
||||
"myCards": "我的网卡",
|
||||
"offlineBatchRecharge": "线下批量充值",
|
||||
"cardTransfer": "网卡转接",
|
||||
"cardReplacement": "换卡管理",
|
||||
"packageGift": "套餐赠送",
|
||||
"cardChange": "换卡网卡"
|
||||
"userCenter": "个人中心"
|
||||
},
|
||||
"packageManagement": {
|
||||
"title": "套餐管理",
|
||||
"packageCreate": "新建套餐",
|
||||
"packageBatch": "批量管理",
|
||||
"packageList": "套餐列表",
|
||||
"packageDetail": "套餐详情",
|
||||
"packageChange": "套餐变更",
|
||||
"seriesGrants": "代理系列授权",
|
||||
"seriesGrantsDetail": "代理系列授权详情",
|
||||
"packageSeries": "套餐系列",
|
||||
"packageSeriesDetail": "套餐系列详情",
|
||||
"packageCommission": "套餐佣金网卡"
|
||||
"packageSeriesDetail": "套餐系列详情"
|
||||
},
|
||||
"accountManagement": {
|
||||
"title": "账号管理",
|
||||
@@ -425,15 +365,6 @@
|
||||
"enterpriseCards": "企业卡管理",
|
||||
"customerCommission": "客户账号佣金"
|
||||
},
|
||||
"product": {
|
||||
"title": "商品管理",
|
||||
"simCard": "号卡管理",
|
||||
"simCardAssign": "号卡分配",
|
||||
"packageSeries": "套餐系列管理",
|
||||
"packageList": "套餐列表",
|
||||
"packageAssign": "套餐分配",
|
||||
"shopAccounts": "店铺账号列表"
|
||||
},
|
||||
"shopManagement": {
|
||||
"title": "店铺管理",
|
||||
"shopList": "店铺列表"
|
||||
@@ -469,20 +400,24 @@
|
||||
"orderList": "订单列表",
|
||||
"orderDetail": "订单详情"
|
||||
},
|
||||
"financialManagement": {
|
||||
"title": "财务管理",
|
||||
"agentRecharge": "代理充值",
|
||||
"agentRechargeDetail": "代理充值详情",
|
||||
"refundManagement": "退款管理",
|
||||
"refundDetail": "退款详情"
|
||||
},
|
||||
"commission": {
|
||||
"menu": {
|
||||
"management": "佣金管理",
|
||||
"withdrawal": "提现审批",
|
||||
"withdrawalSettings": "提现配置",
|
||||
"myCommission": "我的佣金",
|
||||
"agentCommission": "代理商佣金"
|
||||
}
|
||||
"title": "佣金管理",
|
||||
"withdrawal": "提现审批",
|
||||
"withdrawalSettings": "提现配置",
|
||||
"myCommission": "我的佣金",
|
||||
"agentCommission": "代理商佣金"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置管理",
|
||||
"paymentMerchant": "支付商户",
|
||||
"developerApi": "开发者API",
|
||||
"commissionTemplate": "分佣模板"
|
||||
"wechatPayConfiguration": "微信支付配置",
|
||||
"detailsOfPaymentConfiguration": "支付配置详情"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -11,8 +11,6 @@ import { useUserStore } from '@/store/modules/user'
|
||||
*/
|
||||
export const LOGIN_WHITE_LIST = [
|
||||
'/auth/login',
|
||||
'/auth/register',
|
||||
'/auth/forget-password',
|
||||
'/exception/403',
|
||||
'/exception/404',
|
||||
'/exception/500'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,18 +17,6 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
component: () => import('@views/auth/login/index.vue'),
|
||||
meta: { title: 'menus.login.title', isHideTab: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: RoutesAlias.Register,
|
||||
name: 'Register',
|
||||
component: () => import('@views/auth/register/index.vue'),
|
||||
meta: { title: 'menus.register.title', isHideTab: true, noLogin: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: RoutesAlias.ForgetPassword,
|
||||
name: 'ForgetPassword',
|
||||
component: () => import('@views/auth/forget-password/index.vue'),
|
||||
meta: { title: 'menus.forgetPassword.title', isHideTab: true, noLogin: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: '/exception',
|
||||
component: Home,
|
||||
@@ -55,6 +43,35 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/result',
|
||||
name: 'Result',
|
||||
component: Home,
|
||||
meta: {
|
||||
title: 'menus.result.title',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'success',
|
||||
name: 'ResultSuccess',
|
||||
component: () => import('@views/result/success/index.vue'),
|
||||
meta: {
|
||||
title: 'menus.result.success',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'fail',
|
||||
name: 'ResultFail',
|
||||
component: () => import('@views/result/fail/index.vue'),
|
||||
meta: {
|
||||
title: 'menus.result.fail',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/outside',
|
||||
component: Home,
|
||||
|
||||
@@ -34,12 +34,12 @@ export enum RoutesAlias {
|
||||
Shop = '/shop-management/list', // 店铺列表
|
||||
|
||||
// 通用页面(店铺账号-企业账号)
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表(通用)
|
||||
EnterpriseCustomerAccounts = '/common/account-list', // 企业客户账号列表和店铺账号列表共用
|
||||
|
||||
// 账号管理
|
||||
Account = '/account-management/account', // 账号列表
|
||||
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
||||
EnterpriseCards = '/account-management/enterprise-cards', // 企业卡授权
|
||||
EnterpriseCards = '/asset-management/enterprise-cards', // 企业卡授权
|
||||
EnterpriseDevices = '/asset-management/enterprise-devices', // 企业设备授权
|
||||
|
||||
// 资产管理
|
||||
@@ -48,13 +48,17 @@ export enum RoutesAlias {
|
||||
DeviceList = '/asset-management/device-list', // 设备列表
|
||||
ExchangeManagement = '/asset-management/exchange-management', // 换货管理
|
||||
ExchangeDetail = '/asset-management/exchange-management/detail', // 换货单详情
|
||||
AuthorizationRecords = '/asset-management/authorization-records', // 授权记录
|
||||
AuthorizationRecordDetail = '/asset-management/authorization-records/detail', // 授权记录详情
|
||||
AssetAssign = '/asset-management/asset-assign', // 分配记录
|
||||
AssetAssignDetail = '/asset-management/asset-assign/detail', // 分配记录详情
|
||||
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
||||
DeviceTask = '/asset-management/device-task', // 设备任务
|
||||
TaskDetail = '/asset-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
|
||||
// 记录管理
|
||||
AuthorizationRecords = '/asset-management/record-management/authorization-records', // 授权记录
|
||||
AuthorizationRecordDetail = '/asset-management/record-management/authorization-records/detail', // 授权记录详情
|
||||
AssetAssign = '/asset-management/record-management/asset-assign', // 分配记录
|
||||
AssetAssignDetail = '/asset-management/record-management/asset-assign/detail', // 分配记录详情
|
||||
|
||||
// 任务管理
|
||||
IotCardTask = '/asset-management/task-management/iot-card-task', // IoT卡任务
|
||||
DeviceTask = '/asset-management/task-management/device-task', // 设备任务
|
||||
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情(IoT卡/设备任务详情)
|
||||
|
||||
// 订单管理
|
||||
OrderList = '/order-management/order-list', // 订单列表
|
||||
@@ -67,10 +71,10 @@ export enum RoutesAlias {
|
||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||
|
||||
// 佣金管理
|
||||
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
|
||||
CommissionWithdrawalSettings = '/finance/commission/withdrawal-settings', // 提现配置
|
||||
MyCommission = '/finance/commission/my-commission', // 我的佣金
|
||||
AgentCommission = '/finance/commission/agent-commission', // 代理商佣金管理
|
||||
WithdrawalApproval = '/commission-management/withdrawal-approval', // 提现审批
|
||||
CommissionWithdrawalSettings = '/commission-management/withdrawal-settings', // 提现配置
|
||||
MyCommission = '/commission-management/my-commission', // 我的佣金
|
||||
AgentCommission = '/commission-management/agent-commission', // 代理商佣金管理
|
||||
|
||||
// 设置管理
|
||||
WechatConfig = '/settings/wechat-config', // 微信支付配置
|
||||
@@ -85,83 +89,18 @@ export enum RoutesAlias {
|
||||
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
|
||||
PollingMonitor = '/polling-management/monitor', // 轮询监控
|
||||
|
||||
// 其他页面------------------------------------------------暂时不知道有没有用到不要动
|
||||
Register = '/auth/register', // 注册
|
||||
ForgetPassword = '/auth/forget-password', // 忘记密码
|
||||
// 公共组件
|
||||
Success = '/result/success', // 成功
|
||||
Fail = '/result/fail', // 失败
|
||||
IconList = '/widgets/icon-list', // 图标列表
|
||||
IconSelector = '/widgets/icon-selector', // 图标选择器
|
||||
ImageCrop = '/widgets/image-crop', // 图片裁剪
|
||||
Excel = '/widgets/excel', // Excel
|
||||
Video = '/widgets/video', // 视频
|
||||
CountTo = '/widgets/count-to', // 计数
|
||||
WangEditor = '/widgets/wang-editor', // 富文本编辑器
|
||||
Watermark = '/widgets/watermark', // 水印
|
||||
ContextMenu = '/widgets/context-menu', // 上下文菜单
|
||||
Qrcode = '/widgets/qrcode', // 二维码
|
||||
Drag = '/widgets/drag', // 拖拽
|
||||
TextScroll = '/widgets/text-scroll', // 文字滚动
|
||||
Fireworks = '/widgets/fireworks', // 礼花效果
|
||||
Chat = '/template/chat', // 聊天
|
||||
Cards = '/template/cards', // 卡片
|
||||
Banners = '/template/banners', // 横幅
|
||||
Charts = '/template/charts', // 图表
|
||||
Map = '/template/map', // 地图
|
||||
Calendar = '/template/calendar', // 日历
|
||||
Pricing = '/template/pricing', // 定价
|
||||
ArticleList = '/article/list', // 文章列表
|
||||
ArticleDetail = '/article/detail', // 文章详情
|
||||
Comment = '/article/comment', // 评论
|
||||
ArticlePublish = '/article/publish', // 文章发布
|
||||
Server = '/safeguard/server', // 服务器
|
||||
ChangeLog = '/change/log', // 更新日志
|
||||
|
||||
// 物联网卡管理系统模块
|
||||
// 我的网卡
|
||||
CardList = '/card-management/card-list', // 网卡管理
|
||||
CardDetail = '/card-management/card-detail', // 网卡明细
|
||||
|
||||
CardAssign = '/card-management/card-assign', // 网卡分配
|
||||
CardShutdown = '/card-management/card-shutdown', // 停机管理
|
||||
MyCards = '/card-management/my-cards', // 我的网卡
|
||||
OfflineBatchRecharge = '/card-management/offline-batch-recharge', // 线下批量充值
|
||||
CardTransfer = '/card-management/card-transfer', // 网卡转接
|
||||
CardReplacement = '/card-management/card-replacement', // 换卡管理
|
||||
PackageGift = '/card-management/package-gift', // 套餐赠送
|
||||
CardChangeCard = '/card-management/card-change-card', // 换卡网卡
|
||||
|
||||
// other
|
||||
PackageCreate = '/package-management/package-create', // 新建套餐
|
||||
PackageBatch = '/package-management/package-batch', // 批量管理
|
||||
PackageChange = '/package-management/package-change', // 套餐变更
|
||||
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
|
||||
|
||||
// PlatformAccount = '/account-management/platform-account', // 平台账号管理
|
||||
CustomerManagement = '/account-management/customer', // 客户管理
|
||||
CustomerRole = '/account-management/customer-role', // 客户角色
|
||||
AgentManagement = '/account-management/agent', // 代理商管理
|
||||
ShopAccount = '/account-management/shop-account', // 代理账号管理
|
||||
|
||||
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
|
||||
|
||||
// 产品管理
|
||||
SimCardManagement = '/product/sim-card', // 网卡产品管理
|
||||
SimCardAssign = '/product/sim-card-assign', // 号卡分配
|
||||
ShopAccounts = '/common/account-list', // 店铺账号列表(通用)
|
||||
|
||||
// 资产管理
|
||||
DeviceDetail = '/asset-management/device-detail', // 设备详情
|
||||
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
|
||||
|
||||
// 账户管理
|
||||
CustomerAccountList = '/finance/customer-account', // 客户账号
|
||||
MyAccount = '/finance/my-account', // 我的账户
|
||||
|
||||
// 设置管理
|
||||
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
||||
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||
CommissionTemplate = '/settings/commission-template' // 分佣模板
|
||||
Calendar = '/template/calendar' // 日历
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface AgentRechargeQueryParams {
|
||||
status?: AgentRechargeStatus
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
dateRange?: string[] | any // For date range picker in UI
|
||||
}
|
||||
|
||||
// 代理充值订单列表响应
|
||||
@@ -49,7 +50,8 @@ export interface AgentRechargeListResponse {
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
list: AgentRecharge[]
|
||||
items: AgentRecharge[]
|
||||
list: AgentRecharge[] // 兼容旧字段
|
||||
}
|
||||
|
||||
// 创建代理充值订单请求
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface Order {
|
||||
payment_status_text: string
|
||||
payment_method: OrderPaymentMethod
|
||||
total_amount: number // 订单总金额(分)
|
||||
actual_paid_amount?: number // 实付金额(分)
|
||||
paid_at: string | null
|
||||
commission_status: OrderCommissionStatus
|
||||
commission_config_version: number
|
||||
@@ -54,6 +55,9 @@ export interface Order {
|
||||
iot_card_id: number | null
|
||||
asset_identifier?: string // 资产标识符快照(ICCID 或 VirtualNo)
|
||||
asset_type?: string // 资产类型:single_card 或 device
|
||||
purchase_role?: string // 订单渠道
|
||||
purchase_remark?: string // 购买备注
|
||||
operator_name?: string // 操作者
|
||||
items: OrderItem[] | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -67,6 +71,7 @@ export interface OrderQueryParams {
|
||||
order_type?: OrderType
|
||||
order_no?: string
|
||||
identifier?: string // 按资产标识符过滤
|
||||
purchase_role?: string // 订单渠道
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
dateRange?: string[] | any // For date range picker in UI
|
||||
@@ -83,7 +88,10 @@ export interface OrderListResponse {
|
||||
|
||||
// 创建订单请求
|
||||
export interface CreateOrderRequest {
|
||||
identifier: string // 资产标识符(ICCID 或 VirtualNo)
|
||||
order_type?: OrderType // 订单类型
|
||||
iot_card_id?: number | null // IoT卡ID
|
||||
device_id?: number | null // 设备ID
|
||||
identifier?: string // 资产标识符(ICCID 或 VirtualNo)
|
||||
package_ids: number[] // 套餐ID列表
|
||||
payment_method: OrderPaymentMethod // 支付方式
|
||||
}
|
||||
|
||||
4
src/types/components.d.ts
vendored
4
src/types/components.d.ts
vendored
@@ -85,8 +85,10 @@ declare module 'vue' {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
@@ -109,6 +111,7 @@ declare module 'vue' {
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
@@ -120,6 +123,7 @@ declare module 'vue' {
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
|
||||
@@ -1,476 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="代理商名称/联系人" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="levelFilter" placeholder="代理商等级" clearable style="width: 100%">
|
||||
<ElOption label="一级代理" value="1" />
|
||||
<ElOption label="二级代理" value="2" />
|
||||
<ElOption label="三级代理" value="3" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增代理商</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index row-key="id">
|
||||
<template #default>
|
||||
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
|
||||
<ElTableColumn label="代理商编码" prop="agentCode" />
|
||||
<ElTableColumn label="等级" prop="level">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getLevelTagType(scope.row.level)">
|
||||
{{ getLevelText(scope.row.level) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="上级代理" prop="parentName" show-overflow-tooltip />
|
||||
<ElTableColumn label="联系人" prop="contactPerson" />
|
||||
<ElTableColumn label="联系电话" prop="contactPhone" />
|
||||
<ElTableColumn label="账号数量" prop="accountCount" />
|
||||
<ElTableColumn label="网卡数量" prop="simCardCount" />
|
||||
<ElTableColumn label="佣金总额" prop="totalCommission">
|
||||
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="280">
|
||||
<template #default="scope">
|
||||
<el-button link @click="viewAccountList(scope.row)">账号管理</el-button>
|
||||
<el-button link @click="viewCommission(scope.row)">佣金配置</el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增代理商' : '编辑代理商'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="代理商名称" prop="agentName">
|
||||
<ElInput v-model="form.agentName" placeholder="请输入代理商名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="代理商编码" prop="agentCode">
|
||||
<ElInput v-model="form.agentCode" placeholder="请输入代理商编码" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="代理商等级" prop="level">
|
||||
<ElSelect v-model="form.level" placeholder="请选择代理商等级" style="width: 100%">
|
||||
<ElOption label="一级代理" :value="1" />
|
||||
<ElOption label="二级代理" :value="2" />
|
||||
<ElOption label="三级代理" :value="3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级代理" prop="parentId">
|
||||
<ElSelect
|
||||
v-model="form.parentId"
|
||||
placeholder="请选择上级代理"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="无" :value="undefined" />
|
||||
<ElOption
|
||||
v-for="agent in parentAgentOptions"
|
||||
:key="agent.id"
|
||||
:label="agent.agentName"
|
||||
:value="agent.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="联系人" prop="contactPerson">
|
||||
<ElInput v-model="form.contactPerson" placeholder="请输入联系人" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="联系电话" prop="contactPhone">
|
||||
<ElInput v-model="form.contactPhone" placeholder="请输入联系电话" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="邮箱" prop="email">
|
||||
<ElInput v-model="form.email" placeholder="请输入邮箱" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公司地址" prop="address">
|
||||
<ElInput v-model="form.address" placeholder="请输入公司地址" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 账号管理对话框 -->
|
||||
<ElDialog v-model="accountDialogVisible" title="代理商账号管理" width="900px" align-center>
|
||||
<div style="margin-bottom: 16px">
|
||||
<ElButton type="primary" size="small" @click="addSubAccount">创建子账号</ElButton>
|
||||
</div>
|
||||
<ArtTable :data="currentAgentAccounts" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="账号" prop="username" />
|
||||
<ElTableColumn label="真实姓名" prop="realName" />
|
||||
<ElTableColumn label="手机号" prop="phone" />
|
||||
<ElTableColumn label="角色" prop="role" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '正常' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" />
|
||||
<ElTableColumn fixed="right" label="操作" width="150">
|
||||
<template #default>
|
||||
<el-button link>编辑</el-button>
|
||||
<el-button link>禁用</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 佣金配置对话框 -->
|
||||
<ElDialog v-model="commissionDialogVisible" title="佣金配置" width="600px" align-center>
|
||||
<ElForm label-width="120px">
|
||||
<ElFormItem label="佣金模式">
|
||||
<ElRadioGroup v-model="commissionForm.mode">
|
||||
<ElRadio value="fixed">固定佣金</ElRadio>
|
||||
<ElRadio value="percent">比例佣金</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="commissionForm.mode === 'fixed'" label="固定金额">
|
||||
<ElInputNumber v-model="commissionForm.fixedAmount" :min="0" :precision="2" />
|
||||
<span style="margin-left: 8px">元/笔</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="commissionForm.mode === 'percent'" label="佣金比例">
|
||||
<ElInputNumber v-model="commissionForm.percent" :min="0" :max="100" :precision="2" />
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="结算周期">
|
||||
<ElSelect v-model="commissionForm.settlementCycle" style="width: 100%">
|
||||
<ElOption label="实时结算" value="realtime" />
|
||||
<ElOption label="日结" value="daily" />
|
||||
<ElOption label="周结" value="weekly" />
|
||||
<ElOption label="月结" value="monthly" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="commissionDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="saveCommissionConfig">保存</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'AgentManagement' })
|
||||
|
||||
interface Agent {
|
||||
id?: string
|
||||
agentName: string
|
||||
agentCode: string
|
||||
level: number
|
||||
parentId?: string | null
|
||||
parentName?: string
|
||||
contactPerson: string
|
||||
contactPhone: string
|
||||
email?: string
|
||||
address?: string
|
||||
remark?: string
|
||||
accountCount?: number
|
||||
simCardCount?: number
|
||||
totalCommission?: number
|
||||
status: 'active' | 'inactive'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockData = ref<Agent[]>([
|
||||
{
|
||||
id: '1',
|
||||
agentName: '华东区总代理',
|
||||
agentCode: 'AGENT_HD_001',
|
||||
level: 1,
|
||||
parentId: null,
|
||||
parentName: '无',
|
||||
contactPerson: '张三',
|
||||
contactPhone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
address: '上海市浦东新区',
|
||||
accountCount: 5,
|
||||
simCardCount: 1000,
|
||||
totalCommission: 158900.5,
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
agentName: '江苏省代理',
|
||||
agentCode: 'AGENT_JS_001',
|
||||
level: 2,
|
||||
parentId: '1',
|
||||
parentName: '华东区总代理',
|
||||
contactPerson: '李四',
|
||||
contactPhone: '13800138002',
|
||||
email: 'lisi@example.com',
|
||||
address: '江苏省南京市',
|
||||
accountCount: 3,
|
||||
simCardCount: 500,
|
||||
totalCommission: 78500.0,
|
||||
status: 'active',
|
||||
createTime: '2026-01-05 11:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
agentName: '南京市代理',
|
||||
agentCode: 'AGENT_NJ_001',
|
||||
level: 3,
|
||||
parentId: '2',
|
||||
parentName: '江苏省代理',
|
||||
contactPerson: '王五',
|
||||
contactPhone: '13800138003',
|
||||
email: 'wangwu@example.com',
|
||||
address: '江苏省南京市玄武区',
|
||||
accountCount: 2,
|
||||
simCardCount: 200,
|
||||
totalCommission: 32800.0,
|
||||
status: 'active',
|
||||
createTime: '2026-01-10 12:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const levelFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const accountDialogVisible = ref(false)
|
||||
const commissionDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<Agent>({
|
||||
agentName: '',
|
||||
agentCode: '',
|
||||
level: 1,
|
||||
parentId: null,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const commissionForm = reactive({
|
||||
mode: 'fixed',
|
||||
fixedAmount: 10,
|
||||
percent: 5,
|
||||
settlementCycle: 'monthly'
|
||||
})
|
||||
|
||||
const currentAgentAccounts = ref([
|
||||
{
|
||||
username: 'agent001',
|
||||
realName: '张三',
|
||||
phone: '13800138001',
|
||||
role: '管理员',
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
agentName: [{ required: true, message: '请输入代理商名称', trigger: 'blur' }],
|
||||
agentCode: [{ required: true, message: '请输入代理商编码', trigger: 'blur' }],
|
||||
level: [{ required: true, message: '请选择代理商等级', trigger: 'change' }],
|
||||
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
contactPhone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const parentAgentOptions = computed(() => {
|
||||
return mockData.value.filter((item) => item.level < form.level)
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.agentName.includes(searchQuery.value) ||
|
||||
item.contactPerson.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
if (levelFilter.value) {
|
||||
data = data.filter((item) => item.level === parseInt(levelFilter.value))
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getLevelText = (level: number) => {
|
||||
const levelMap: Record<number, string> = { 1: '一级代理', 2: '二级代理', 3: '三级代理' }
|
||||
return levelMap[level] || '未知'
|
||||
}
|
||||
|
||||
const getLevelTagType = (level: number) => {
|
||||
const typeMap: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'warning' }
|
||||
return typeMap[level] || 'info'
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Agent) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, row)
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
agentName: '',
|
||||
agentCode: '',
|
||||
level: 1,
|
||||
parentId: null,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
remark: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
parentName: parentAgent ? parentAgent.agentName : '无',
|
||||
accountCount: 0,
|
||||
simCardCount: 0,
|
||||
totalCommission: 0,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) {
|
||||
const parentAgent = mockData.value.find((item) => item.id === form.parentId)
|
||||
mockData.value[index] = {
|
||||
...form,
|
||||
parentName: parentAgent ? parentAgent.agentName : '无'
|
||||
}
|
||||
}
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Agent) => {
|
||||
ElMessageBox.confirm('确定删除该代理商吗?删除后其下属代理商和账号也将被删除。', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) {
|
||||
mockData.value.splice(index, 1)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewAccountList = (row: Agent) => {
|
||||
accountDialogVisible.value = true
|
||||
// 实际应用中应该根据代理商ID加载账号列表
|
||||
console.log(row)
|
||||
}
|
||||
|
||||
const viewCommission = (row: Agent) => {
|
||||
commissionDialogVisible.value = true
|
||||
// 实际应用中应该根据代理商ID加载佣金配置
|
||||
console.log(row)
|
||||
}
|
||||
|
||||
const addSubAccount = () => {
|
||||
ElMessage.info('创建子账号功能')
|
||||
}
|
||||
|
||||
const saveCommissionConfig = () => {
|
||||
ElMessage.success('佣金配置保存成功')
|
||||
commissionDialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
// 自定义样式
|
||||
}
|
||||
</style>
|
||||
@@ -1,457 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 统计卡片 -->
|
||||
<ElRow :gutter="20" style="margin-bottom: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">客户总数</div>
|
||||
<div class="stat-value">{{ statistics.totalCustomers }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><User /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">累计佣金</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">
|
||||
¥{{ statistics.totalCommission.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"><Money /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已提现</div>
|
||||
<div class="stat-value" style="color: var(--el-color-warning)">
|
||||
¥{{ statistics.totalWithdrawn.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"
|
||||
><WalletFilled
|
||||
/></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待提现</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">
|
||||
¥{{ statistics.pendingWithdrawal.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><Wallet /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 搜索和筛选区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="客户名称/手机号" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="customerTypeFilter" placeholder="客户类型" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="代理商" value="agent" />
|
||||
<ElOption label="企业客户" value="enterprise" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect
|
||||
v-model="commissionRangeFilter"
|
||||
placeholder="佣金范围"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="0-1000元" value="0-1000" />
|
||||
<ElOption label="1000-5000元" value="1000-5000" />
|
||||
<ElOption label="5000-10000元" value="5000-10000" />
|
||||
<ElOption label="10000元以上" value="10000+" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="exportData">导出</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 客户佣金列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||||
<template #default>
|
||||
<ElTableColumn label="客户名称" prop="customerName" min-width="150" show-overflow-tooltip />
|
||||
<ElTableColumn label="客户类型" prop="customerType" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.customerType === 'agent' ? 'warning' : 'primary'">
|
||||
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="联系电话" prop="phone" width="130" />
|
||||
<ElTableColumn label="累计佣金" prop="totalCommission" width="130" sortable>
|
||||
<template #default="scope">
|
||||
<span style="font-weight: 600; color: var(--el-color-success)">
|
||||
¥{{ scope.row.totalCommission.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已提现" prop="withdrawnAmount" width="130" sortable>
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-warning)">
|
||||
¥{{ scope.row.withdrawnAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="待提现" prop="pendingAmount" width="130" sortable>
|
||||
<template #default="scope">
|
||||
<span style="font-weight: 600; color: var(--el-color-danger)">
|
||||
¥{{ scope.row.pendingAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现次数" prop="withdrawalCount" width="100" align="center" />
|
||||
<ElTableColumn label="卡片数量" prop="cardCount" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.cardCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="最近提现" prop="lastWithdrawalTime" width="180" />
|
||||
<ElTableColumn label="注册时间" prop="registerTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="220">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewCommissionDetail(scope.row)"
|
||||
>佣金详情</el-button
|
||||
>
|
||||
<el-button link :icon="List" @click="viewWithdrawalHistory(scope.row)"
|
||||
>提现记录</el-button
|
||||
>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 佣金详情对话框 -->
|
||||
<ElDialog v-model="commissionDialogVisible" title="佣金详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border style="margin-bottom: 20px">
|
||||
<ElDescriptionsItem label="客户名称">{{ currentCustomer.customerName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户类型">
|
||||
<ElTag :type="currentCustomer.customerType === 'agent' ? 'warning' : 'primary'">
|
||||
{{ currentCustomer.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="累计佣金">
|
||||
<span style="font-weight: 600; color: var(--el-color-success)">
|
||||
¥{{ currentCustomer.totalCommission.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已提现">
|
||||
<span style="color: var(--el-color-warning)">
|
||||
¥{{ currentCustomer.withdrawnAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="待提现">
|
||||
<span style="font-weight: 600; color: var(--el-color-danger)">
|
||||
¥{{ currentCustomer.pendingAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提现次数">{{
|
||||
currentCustomer.withdrawalCount
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">佣金明细</ElDivider>
|
||||
<ArtTable :data="commissionDetails" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="来源" prop="source" width="120" />
|
||||
<ElTableColumn label="订单号" prop="orderNo" width="180" />
|
||||
<ElTableColumn label="佣金金额" prop="amount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)"
|
||||
>+¥{{ scope.row.amount.toFixed(2) }}</span
|
||||
>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金比例" prop="rate" width="100" />
|
||||
<ElTableColumn label="订单金额" prop="orderAmount" width="120" />
|
||||
<ElTableColumn label="获得时间" prop="createTime" width="180" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'completed'" type="success">已结算</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'pending'" type="warning">待结算</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="commissionDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 提现记录对话框 -->
|
||||
<ElDialog v-model="withdrawalDialogVisible" title="提现记录" width="900px" align-center>
|
||||
<ArtTable :data="withdrawalHistory" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="提现单号" prop="withdrawalNo" width="180" />
|
||||
<ElTableColumn label="提现金额" prop="amount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="font-weight: 600; color: var(--el-color-danger)">
|
||||
¥{{ scope.row.amount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="手续费" prop="fee" width="100">
|
||||
<template #default="scope"> ¥{{ scope.row.fee.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="实际到账" prop="actualAmount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">
|
||||
¥{{ scope.row.actualAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现方式" prop="method" width="100" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'processing'" type="warning">处理中</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'pending'" type="info">待审核</ElTag>
|
||||
<ElTag v-else type="danger">已拒绝</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
|
||||
<ElTableColumn label="完成时间" prop="completeTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.completeTime || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="withdrawalDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View, List, User, Money, WalletFilled, Wallet } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({ name: 'CustomerCommission' })
|
||||
|
||||
interface CustomerCommission {
|
||||
id: string
|
||||
customerName: string
|
||||
customerType: 'agent' | 'enterprise'
|
||||
phone: string
|
||||
totalCommission: number
|
||||
withdrawnAmount: number
|
||||
pendingAmount: number
|
||||
withdrawalCount: number
|
||||
cardCount: number
|
||||
lastWithdrawalTime: string
|
||||
registerTime: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const customerTypeFilter = ref('')
|
||||
const commissionRangeFilter = ref('')
|
||||
const commissionDialogVisible = ref(false)
|
||||
const withdrawalDialogVisible = ref(false)
|
||||
|
||||
const statistics = reactive({
|
||||
totalCustomers: 156,
|
||||
totalCommission: 580000.0,
|
||||
totalWithdrawn: 420000.0,
|
||||
pendingWithdrawal: 160000.0
|
||||
})
|
||||
|
||||
const mockData = ref<CustomerCommission[]>([
|
||||
{
|
||||
id: '1',
|
||||
customerName: '华东区总代理',
|
||||
customerType: 'agent',
|
||||
phone: '13800138000',
|
||||
totalCommission: 85000.0,
|
||||
withdrawnAmount: 70000.0,
|
||||
pendingAmount: 15000.0,
|
||||
withdrawalCount: 12,
|
||||
cardCount: 500,
|
||||
lastWithdrawalTime: '2026-01-08 10:00:00',
|
||||
registerTime: '2025-06-01 09:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
customerName: '深圳市科技有限公司',
|
||||
customerType: 'enterprise',
|
||||
phone: '13900139000',
|
||||
totalCommission: 45000.0,
|
||||
withdrawnAmount: 30000.0,
|
||||
pendingAmount: 15000.0,
|
||||
withdrawalCount: 8,
|
||||
cardCount: 300,
|
||||
lastWithdrawalTime: '2026-01-05 14:30:00',
|
||||
registerTime: '2025-07-15 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
customerName: '北京智能制造',
|
||||
customerType: 'enterprise',
|
||||
phone: '13700137000',
|
||||
totalCommission: 68000.0,
|
||||
withdrawnAmount: 55000.0,
|
||||
pendingAmount: 13000.0,
|
||||
withdrawalCount: 10,
|
||||
cardCount: 450,
|
||||
lastWithdrawalTime: '2026-01-07 16:00:00',
|
||||
registerTime: '2025-08-20 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const currentCustomer = ref<CustomerCommission>({
|
||||
id: '',
|
||||
customerName: '',
|
||||
customerType: 'agent',
|
||||
phone: '',
|
||||
totalCommission: 0,
|
||||
withdrawnAmount: 0,
|
||||
pendingAmount: 0,
|
||||
withdrawalCount: 0,
|
||||
cardCount: 0,
|
||||
lastWithdrawalTime: '',
|
||||
registerTime: ''
|
||||
})
|
||||
|
||||
const commissionDetails = ref([
|
||||
{
|
||||
id: '1',
|
||||
source: '套餐销售',
|
||||
orderNo: 'ORD202601090001',
|
||||
amount: 150.0,
|
||||
rate: '10%',
|
||||
orderAmount: '¥1,500.00',
|
||||
createTime: '2026-01-09 09:30:00',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
source: '卡片激活',
|
||||
orderNo: 'ORD202601080025',
|
||||
amount: 80.0,
|
||||
rate: '8%',
|
||||
orderAmount: '¥1,000.00',
|
||||
createTime: '2026-01-08 15:20:00',
|
||||
status: 'completed'
|
||||
}
|
||||
])
|
||||
|
||||
const withdrawalHistory = ref([
|
||||
{
|
||||
id: '1',
|
||||
withdrawalNo: 'WD202601080001',
|
||||
amount: 10000.0,
|
||||
fee: 20.0,
|
||||
actualAmount: 9980.0,
|
||||
method: '银行卡',
|
||||
status: 'completed',
|
||||
applyTime: '2026-01-08 10:00:00',
|
||||
completeTime: '2026-01-08 15:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
withdrawalNo: 'WD202601050002',
|
||||
amount: 5000.0,
|
||||
fee: 10.0,
|
||||
actualAmount: 4990.0,
|
||||
method: '支付宝',
|
||||
status: 'completed',
|
||||
applyTime: '2026-01-05 14:00:00',
|
||||
completeTime: '2026-01-05 18:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.customerName.includes(searchQuery.value) || item.phone.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (customerTypeFilter.value) {
|
||||
data = data.filter((item) => item.customerType === customerTypeFilter.value)
|
||||
}
|
||||
|
||||
if (commissionRangeFilter.value) {
|
||||
data = data.filter((item) => {
|
||||
const commission = item.totalCommission
|
||||
if (commissionRangeFilter.value === '0-1000') return commission >= 0 && commission < 1000
|
||||
if (commissionRangeFilter.value === '1000-5000')
|
||||
return commission >= 1000 && commission < 5000
|
||||
if (commissionRangeFilter.value === '5000-10000')
|
||||
return commission >= 5000 && commission < 10000
|
||||
if (commissionRangeFilter.value === '10000+') return commission >= 10000
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const exportData = () => {
|
||||
ElMessage.success('数据导出中...')
|
||||
}
|
||||
|
||||
const viewCommissionDetail = (row: CustomerCommission) => {
|
||||
currentCustomer.value = { ...row }
|
||||
commissionDialogVisible.value = true
|
||||
}
|
||||
|
||||
const viewWithdrawalHistory = (row: CustomerCommission) => {
|
||||
currentCustomer.value = { ...row }
|
||||
withdrawalDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="角色名称" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增角色</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="角色名称" prop="roleName" />
|
||||
<ElTableColumn label="角色编码" prop="roleCode" />
|
||||
<ElTableColumn label="能力边界" prop="abilities">
|
||||
<template #default="scope">
|
||||
<ElTag
|
||||
v-for="(ability, index) in scope.row.abilities"
|
||||
:key="index"
|
||||
size="small"
|
||||
style="margin-right: 4px"
|
||||
>
|
||||
{{ ability }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="描述" prop="description" show-overflow-tooltip />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增客户角色' : '编辑客户角色'"
|
||||
width="600px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="roleName">
|
||||
<ElInput v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色编码" prop="roleCode">
|
||||
<ElInput v-model="form.roleCode" placeholder="请输入角色编码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="能力边界" prop="abilities">
|
||||
<ElCheckboxGroup v-model="form.abilities">
|
||||
<ElCheckbox label="查看网卡">查看网卡</ElCheckbox>
|
||||
<ElCheckbox label="操作网卡">操作网卡</ElCheckbox>
|
||||
<ElCheckbox label="查看套餐">查看套餐</ElCheckbox>
|
||||
<ElCheckbox label="购买套餐">购买套餐</ElCheckbox>
|
||||
<ElCheckbox label="查看设备">查看设备</ElCheckbox>
|
||||
<ElCheckbox label="管理设备">管理设备</ElCheckbox>
|
||||
<ElCheckbox label="查看佣金">查看佣金</ElCheckbox>
|
||||
<ElCheckbox label="提现佣金">提现佣金</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CustomerRole' })
|
||||
|
||||
interface CustomerRole {
|
||||
id?: string
|
||||
roleName: string
|
||||
roleCode: string
|
||||
abilities: string[]
|
||||
description: string
|
||||
status: 'active' | 'inactive'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockData = ref<CustomerRole[]>([
|
||||
{
|
||||
id: '1',
|
||||
roleName: '标准客户',
|
||||
roleCode: 'CUSTOMER_STANDARD',
|
||||
abilities: ['查看网卡', '查看套餐', '查看设备'],
|
||||
description: '标准客户角色,拥有基本查看权限',
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
roleName: 'VIP客户',
|
||||
roleCode: 'CUSTOMER_VIP',
|
||||
abilities: ['查看网卡', '操作网卡', '查看套餐', '购买套餐', '查看设备', '管理设备'],
|
||||
description: 'VIP客户角色,拥有更多操作权限',
|
||||
status: 'active',
|
||||
createTime: '2026-01-02 11:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
roleName: '企业客户',
|
||||
roleCode: 'CUSTOMER_ENTERPRISE',
|
||||
abilities: [
|
||||
'查看网卡',
|
||||
'操作网卡',
|
||||
'查看套餐',
|
||||
'购买套餐',
|
||||
'查看设备',
|
||||
'管理设备',
|
||||
'查看佣金'
|
||||
],
|
||||
description: '企业客户角色,拥有完整业务权限',
|
||||
status: 'active',
|
||||
createTime: '2026-01-03 12:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<CustomerRole>({
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
abilities: [],
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
abilities: [{ required: true, message: '请至少选择一项能力', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchQuery.value) return mockData.value
|
||||
return mockData.value.filter((item) =>
|
||||
item.roleName.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已通过 computed 实现
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: CustomerRole) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.id = row.id
|
||||
form.roleName = row.roleName
|
||||
form.roleCode = row.roleCode
|
||||
form.abilities = [...row.abilities]
|
||||
form.description = row.description
|
||||
form.status = row.status
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.id = undefined
|
||||
form.roleName = ''
|
||||
form.roleCode = ''
|
||||
form.abilities = []
|
||||
form.description = ''
|
||||
form.status = 'active'
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) {
|
||||
mockData.value[index] = { ...form }
|
||||
}
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: CustomerRole) => {
|
||||
ElMessageBox.confirm('确定删除该客户角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) {
|
||||
mockData.value.splice(index, 1)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,359 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="customer-management-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">查询</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CustomerManagement' })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
distributorName: '',
|
||||
distributorAccount: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据 - 客户管理(代理商)
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
distributorName: '北京优享科技有限公司',
|
||||
distributorAccount: 'bjyx2024',
|
||||
totalCommission: 12560.8,
|
||||
instantCommission: 8320.5,
|
||||
pendingCommission: 4240.3,
|
||||
prepaidCommission: 2400.0,
|
||||
withdrawnCommission: 6800.2,
|
||||
withdrawingCommission: 1520.3,
|
||||
availableCommission: 2000.0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
distributorName: '上海智联通信技术公司',
|
||||
distributorAccount: 'shzl2024',
|
||||
totalCommission: 18750.25,
|
||||
instantCommission: 12500.75,
|
||||
pendingCommission: 6249.5,
|
||||
prepaidCommission: 3600.0,
|
||||
withdrawnCommission: 8900.25,
|
||||
withdrawingCommission: 2100.0,
|
||||
availableCommission: 3750.0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
distributorName: '广州物联网络有限公司',
|
||||
distributorAccount: 'gzwl2024',
|
||||
totalCommission: 9876.4,
|
||||
instantCommission: 6584.3,
|
||||
pendingCommission: 3292.1,
|
||||
prepaidCommission: 1800.0,
|
||||
withdrawnCommission: 4938.2,
|
||||
withdrawingCommission: 980.2,
|
||||
availableCommission: 1958.0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
distributorName: '深圳云联科技股份公司',
|
||||
distributorAccount: 'szyl2024',
|
||||
totalCommission: 24500.6,
|
||||
instantCommission: 16333.73,
|
||||
pendingCommission: 8166.87,
|
||||
prepaidCommission: 4800.0,
|
||||
withdrawnCommission: 11225.3,
|
||||
withdrawingCommission: 2450.06,
|
||||
availableCommission: 4900.12
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
distributorName: '杭州通达网络服务公司',
|
||||
distributorAccount: 'hztd2024',
|
||||
totalCommission: 15200.35,
|
||||
instantCommission: 10133.57,
|
||||
pendingCommission: 5066.78,
|
||||
prepaidCommission: 2900.0,
|
||||
withdrawnCommission: 7600.18,
|
||||
withdrawingCommission: 1520.04,
|
||||
availableCommission: 3040.07
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '代理商名称',
|
||||
prop: 'distributorName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入代理商名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '代理商账号',
|
||||
prop: 'distributorAccount',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入代理商账号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '代理商名称', prop: 'distributorName' },
|
||||
{ label: '代理商账号', prop: 'distributorAccount' },
|
||||
{ label: '总佣金', prop: 'totalCommission' },
|
||||
{ label: '已秒返佣金', prop: 'instantCommission' },
|
||||
{ label: '未秒返佣金', prop: 'pendingCommission' },
|
||||
{ label: '预存佣金', prop: 'prepaidCommission' },
|
||||
{ label: '已提现佣金', prop: 'withdrawnCommission' },
|
||||
{ label: '提现中佣金', prop: 'withdrawingCommission' },
|
||||
{ label: '可提现佣金', prop: 'availableCommission' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条客户记录`)
|
||||
}
|
||||
|
||||
// 查看提现详情
|
||||
const viewWithdrawDetail = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.distributorName} 的提现详情`)
|
||||
}
|
||||
|
||||
// 查看佣金明细
|
||||
const viewCommissionDetail = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.distributorName} 的佣金明细`)
|
||||
}
|
||||
|
||||
// 查看自动提现详情
|
||||
const viewAutoWithdrawDetail = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.distributorName} 的自动提现详情`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '代理商名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'distributorAccount',
|
||||
label: '代理商账号',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'totalCommission',
|
||||
label: '总佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.totalCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'instantCommission',
|
||||
label: '已秒返佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.instantCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'pendingCommission',
|
||||
label: '未秒返佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.pendingCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'prepaidCommission',
|
||||
label: '预存佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.prepaidCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'withdrawnCommission',
|
||||
label: '已提现佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.withdrawnCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'withdrawingCommission',
|
||||
label: '提现中佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.withdrawingCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'availableCommission',
|
||||
label: '可提现佣金',
|
||||
width: 120,
|
||||
formatter: (row) => `¥${row.availableCommission.toFixed(2)}`
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 200,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { style: 'display: flex; gap: 5px; flex-wrap: wrap;' }, [
|
||||
h(ArtButtonTable, {
|
||||
type: 'view',
|
||||
text: '提现详情',
|
||||
onClick: () => viewWithdrawDetail(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'detail',
|
||||
text: '佣金明细',
|
||||
onClick: () => viewCommissionDetail(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'info',
|
||||
text: '自动提现详情',
|
||||
onClick: () => viewAutoWithdrawDetail(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCustomerList()
|
||||
})
|
||||
|
||||
// 获取客户列表
|
||||
const getCustomerList = 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
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCustomerList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCustomerList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.customer-management-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
</style>
|
||||
@@ -207,6 +207,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { EnterpriseService, ShopService } from '@/api/modules'
|
||||
import { ElMessage, ElSwitch, ElCascader, ElTreeSelect } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
@@ -797,7 +798,7 @@
|
||||
// 查看客户账号
|
||||
const viewCustomerAccounts = (row: EnterpriseItem) => {
|
||||
router.push({
|
||||
path: `/account-management/enterprise-customer/customer-accounts/${row.id}`,
|
||||
path: `${RoutesAlias.EnterpriseCustomer}/customer-accounts/${row.id}`,
|
||||
query: { type: 'enterprise' }
|
||||
})
|
||||
}
|
||||
@@ -805,14 +806,14 @@
|
||||
// 卡管理
|
||||
const manageCards = (row: EnterpriseItem) => {
|
||||
router.push({
|
||||
path: '/account-management/enterprise-cards',
|
||||
path: RoutesAlias.EnterpriseCards,
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
const manageDevices = (row: EnterpriseItem) => {
|
||||
router.push({
|
||||
path: '/asset-management/enterprise-devices',
|
||||
path: RoutesAlias.EnterpriseDevices,
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,599 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="shop-account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<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 @click="showDialog('add')" v-permission="'shop_account:add'"
|
||||
>新增代理账号</ElButton
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="500px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||
<ElFormItem label="用户名" prop="username">
|
||||
<ElInput v-model="formData.username" placeholder="请输入用户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
|
||||
<ElInput
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="店铺" prop="shop_id">
|
||||
<ElSelect
|
||||
v-model="formData.shop_id"
|
||||
placeholder="请选择店铺"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchShops"
|
||||
:loading="shopLoading"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="shop in shopList"
|
||||
:key="shop.id"
|
||||
:label="shop.shop_name"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading"
|
||||
>提交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 修改密码对话框 -->
|
||||
<ElDialog v-model="passwordDialogVisible" title="重置密码" width="400px">
|
||||
<ElForm ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
|
||||
<ElFormItem label="新密码" prop="new_password">
|
||||
<ElInput
|
||||
v-model="passwordForm.new_password"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="passwordDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleChangePassword"
|
||||
:loading="passwordSubmitLoading"
|
||||
>提交</ElButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { FormInstance, ElMessage, ElMessageBox, ElSwitch, ElSelect, ElOption } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { AccountService, ShopService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PlatformAccount, ShopResponse } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
|
||||
defineOptions({ name: 'ShopAccount' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const passwordDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const passwordSubmitLoading = ref(false)
|
||||
const shopLoading = ref(false)
|
||||
const currentAccountId = ref<number>(0)
|
||||
const shopList = ref<ShopResponse[]>([])
|
||||
const searchShopList = ref<ShopResponse[]>([])
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
username: '',
|
||||
phone: '',
|
||||
shop_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<PlatformAccount[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', searchForm)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '用户名',
|
||||
prop: 'username',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入用户名'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入手机号'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
options: searchShopList.value.map((shop) => ({ label: shop.shop_name, value: shop.id })),
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: handleSearchShop,
|
||||
loading: shopLoading.value,
|
||||
placeholder: '请选择或搜索店铺'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
options: STATUS_SELECT_OPTIONS,
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '用户名', prop: 'username' },
|
||||
{ label: '手机号', prop: 'phone' },
|
||||
{ label: '店铺名称', prop: 'shop_name' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: PlatformAccount) => {
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.id = row.id
|
||||
formData.username = row.username
|
||||
formData.phone = row.phone
|
||||
formData.shop_id = row.shop_id || 0
|
||||
formData.password = ''
|
||||
} else {
|
||||
formData.id = 0
|
||||
formData.username = ''
|
||||
formData.phone = ''
|
||||
formData.shop_id = 0
|
||||
formData.password = ''
|
||||
}
|
||||
|
||||
// 重置表单验证状态
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示修改密码对话框
|
||||
const showPasswordDialog = (row: PlatformAccount) => {
|
||||
currentAccountId.value = row.id
|
||||
passwordForm.new_password = ''
|
||||
|
||||
// 重置表单验证状态
|
||||
nextTick(() => {
|
||||
passwordFormRef.value?.clearValidate()
|
||||
})
|
||||
|
||||
passwordDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 状态切换处理
|
||||
const handleStatusChange = async (row: PlatformAccount, newStatus: number) => {
|
||||
const oldStatus = row.status
|
||||
// 先更新UI
|
||||
row.status = newStatus
|
||||
try {
|
||||
await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
|
||||
ElMessage.success('状态切换成功')
|
||||
} catch (error) {
|
||||
// 切换失败,恢复原状态
|
||||
row.status = oldStatus
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID'
|
||||
},
|
||||
{
|
||||
prop: 'username',
|
||||
label: '用户名'
|
||||
},
|
||||
{
|
||||
prop: 'phone',
|
||||
label: '手机号'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺名称',
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row: PlatformAccount) => {
|
||||
return h(ElSwitch, {
|
||||
modelValue: row.status,
|
||||
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: '创建时间',
|
||||
formatter: (row: PlatformAccount) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
formatter: (row: PlatformAccount) => {
|
||||
const buttons = []
|
||||
|
||||
if (hasAuth('shop_account:update_psd')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
icon: '',
|
||||
onClick: () => showPasswordDialog(row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (hasAuth('shop_account:edit')) {
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const passwordFormRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
shop_id: 0
|
||||
})
|
||||
|
||||
// 密码表单数据
|
||||
const passwordForm = reactive({
|
||||
new_password: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getAccountList()
|
||||
loadShopList()
|
||||
loadSearchShopList()
|
||||
})
|
||||
|
||||
// 加载店铺列表(用于新增/编辑对话框,默认加载20条)
|
||||
const loadShopList = async (shopName?: string) => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
if (shopName) {
|
||||
params.shop_name = shopName
|
||||
}
|
||||
const res = await ShopService.getShops(params)
|
||||
if (res.code === 0) {
|
||||
shopList.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺列表失败:', error)
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载搜索栏店铺列表(默认加载20条)
|
||||
const loadSearchShopList = async (shopName?: string) => {
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
if (shopName) {
|
||||
params.shop_name = shopName
|
||||
}
|
||||
const res = await ShopService.getShops(params)
|
||||
if (res.code === 0) {
|
||||
searchShopList.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索店铺(用于新增/编辑对话框)
|
||||
const searchShops = (query: string) => {
|
||||
if (query) {
|
||||
loadShopList(query)
|
||||
} else {
|
||||
loadShopList()
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索店铺(用于搜索栏)
|
||||
const handleSearchShop = (query: string) => {
|
||||
if (query) {
|
||||
loadSearchShopList(query)
|
||||
} else {
|
||||
loadSearchShopList()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交修改密码
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
await passwordFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
passwordSubmitLoading.value = true
|
||||
try {
|
||||
await AccountService.updateAccountPassword(
|
||||
currentAccountId.value,
|
||||
passwordForm.new_password
|
||||
)
|
||||
ElMessage.success('重置密码成功')
|
||||
passwordDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
passwordSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取账号列表
|
||||
const getAccountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
user_type: 3, // 筛选代理账号
|
||||
username: searchForm.username || undefined,
|
||||
phone: searchForm.phone || undefined,
|
||||
shop_id: searchForm.shop_id,
|
||||
status: searchForm.status
|
||||
}
|
||||
const res = await AccountService.getAccounts(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取代理账号列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
shop_id: [
|
||||
{ required: true, message: '请选择店铺', trigger: 'change' },
|
||||
{ type: 'number', min: 0, message: '店铺ID必须大于0', trigger: 'change' }
|
||||
]
|
||||
})
|
||||
|
||||
// 密码验证规则
|
||||
const passwordRules = reactive<FormRules>({
|
||||
new_password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const data = {
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
phone: formData.phone,
|
||||
user_type: 3, // 代理账号
|
||||
shop_id: formData.shop_id
|
||||
}
|
||||
|
||||
await AccountService.createAccount(data)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const data = {
|
||||
username: formData.username
|
||||
}
|
||||
|
||||
await AccountService.updateAccount(formData.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getAccountList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shop-account-page {
|
||||
// 代理账号管理页面样式
|
||||
}
|
||||
</style>
|
||||
@@ -1,269 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1 class="title">留言墙</h1>
|
||||
<p class="desc">每一份留言都记录了您的想法,也为我们提供了珍贵的回忆</p>
|
||||
|
||||
<div class="list">
|
||||
<ul class="offset">
|
||||
<li
|
||||
class="comment-box"
|
||||
v-for="item in commentList"
|
||||
:key="item.id"
|
||||
:style="{ background: randomColor() }"
|
||||
@click="openDrawer(item)"
|
||||
>
|
||||
<p class="date">{{ item.date }}</p>
|
||||
<p class="content">{{ item.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ item.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ item.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ item.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ElDrawer
|
||||
lDrawer
|
||||
v-model="showDrawer"
|
||||
:lock-scroll="false"
|
||||
:size="360"
|
||||
modal-class="comment-modal"
|
||||
>
|
||||
<template #header>
|
||||
<h4>详情</h4>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="drawer-default">
|
||||
<div class="comment-box" :style="{ background: randomColor() }">
|
||||
<p class="date">{{ clickItem.date }}</p>
|
||||
<p class="content">{{ clickItem.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ clickItem.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论组件 -->
|
||||
<CommentWidget />
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<!-- <ElButton @click="cancelClick">cancel</ElButton> -->
|
||||
<!-- <ElButton type="primary" @click="confirmClick">confirm</ElButton> -->
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commentList } from '@/mock/temp/commentList'
|
||||
const showDrawer = ref(false)
|
||||
|
||||
defineOptions({ name: 'ArticleComment' })
|
||||
|
||||
// const colorList = reactive([
|
||||
// 'rgba(216, 248, 255, 0.8)',
|
||||
// 'rgba(253, 223, 217, 0.8)',
|
||||
// 'rgba(252, 230, 240, 0.8)',
|
||||
// 'rgba(211, 248, 240, 0.8)',
|
||||
// 'rgba(255, 234, 188, 0.8)',
|
||||
// 'rgba(245, 225, 255, 0.8)',
|
||||
// 'rgba(225, 230, 254, 0.8)'
|
||||
// ])
|
||||
|
||||
const colorList = reactive([
|
||||
'#D8F8FF',
|
||||
'#FDDFD9',
|
||||
'#FCE6F0',
|
||||
'#D3F8F0',
|
||||
'#FFEABC',
|
||||
'#F5E1FF',
|
||||
'#E1E6FE'
|
||||
])
|
||||
|
||||
let lastColor: string | null = null
|
||||
|
||||
const randomColor = () => {
|
||||
let newColor: string
|
||||
|
||||
do {
|
||||
const index = Math.floor(Math.random() * colorList.length)
|
||||
newColor = colorList[index]
|
||||
} while (newColor === lastColor)
|
||||
|
||||
lastColor = newColor
|
||||
return newColor
|
||||
}
|
||||
|
||||
const clickItem = ref({
|
||||
id: 1,
|
||||
date: '2024-9-3',
|
||||
content: '加油!学好Node 自己写个小Demo',
|
||||
collection: 5,
|
||||
comment: 8,
|
||||
userName: '匿名'
|
||||
})
|
||||
|
||||
const openDrawer = (item: any) => {
|
||||
showDrawer.value = true
|
||||
clickItem.value = item
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
:deep(.comment-modal) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 20px;
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 40px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 16px);
|
||||
aspect-ratio: 16 / 12;
|
||||
padding: 16px;
|
||||
margin: 0 16px 16px 0;
|
||||
cursor: pointer;
|
||||
background-color: #eae2cb;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-default {
|
||||
.comment-box {
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-notebook) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(25% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(33.333% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(50% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<div class="article-detail page-content">
|
||||
<div class="content">
|
||||
<h1>{{ articleTitle }}</h1>
|
||||
<div class="markdown-body" v-highlight v-html="articleHtml"></div>
|
||||
</div>
|
||||
<ArtBackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '@/assets/styles/markdown.scss'
|
||||
import '@/assets/styles/one-dark-pro.scss'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import axios from 'axios'
|
||||
// import 'highlight.js/styles/atom-one-dark.css';
|
||||
// import 'highlight.js/styles/vs2015.css';
|
||||
|
||||
defineOptions({ name: 'ArticleDetail' })
|
||||
|
||||
const articleId = ref(0)
|
||||
const router = useRoute()
|
||||
const articleTitle = ref('')
|
||||
const articleHtml = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
articleId.value = Number(router.query.id)
|
||||
getArticleDetail()
|
||||
})
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
if (articleId.value) {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_detail.json')
|
||||
if (res.data.code === 200) {
|
||||
articleTitle.value = res.data.data.title
|
||||
articleHtml.value = res.data.data.html_content
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleDetail(articleId.value)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTitle.value = res.data.title;;
|
||||
// articleHtml.value = res.data.html_content;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.article-detail {
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin-top: 60px;
|
||||
|
||||
.markdown-body {
|
||||
margin-top: 60px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 1px solid var(--art-gray-200);
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: #0a0a0e;
|
||||
}
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #9e9e9e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,380 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content article-list">
|
||||
<ElRow justify="space-between" :gutter="10">
|
||||
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
|
||||
<ElInput
|
||||
v-model="searchVal"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="输入文章标题查询"
|
||||
@keyup.enter="searchArticle"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
|
||||
<div class="custom-segmented">
|
||||
<ElSegmented v-model="yearVal" :options="options" @change="searchArticleByYear" />
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="6" :sm="10" :xs="6" style="display: flex; justify-content: end">
|
||||
<ElButton @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div class="list">
|
||||
<div class="offset">
|
||||
<div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
|
||||
<!-- 骨架屏 -->
|
||||
<ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
|
||||
<template #template>
|
||||
<div class="top">
|
||||
<ElSkeletonItem
|
||||
variant="image"
|
||||
style="width: 100%; height: 100%; border-radius: 10px"
|
||||
/>
|
||||
<div style="padding: 16px 0">
|
||||
<ElSkeletonItem variant="p" style="width: 80%" />
|
||||
<ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="top">
|
||||
<ElImage class="cover" :src="item.home_img" lazy fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<ElIcon><icon-picture /></ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
|
||||
<span class="type">{{ item.type_name }}</span>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<div class="info">
|
||||
<div class="text">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
|
||||
<div class="line"></div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ item.count }}</span>
|
||||
</div>
|
||||
<ElButton v-auth="'edit'" size="small" @click.stop="toEdit(item)">编辑</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElSkeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16vh" v-if="showEmpty">
|
||||
<ElEmpty description="未找到相关数据" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 20px">
|
||||
<ElPagination
|
||||
size="default"
|
||||
background
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="9"
|
||||
layout="prev, pager, next, total,jumper"
|
||||
:total="total"
|
||||
:hide-on-single-page="true"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue'
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { router } from '@/router'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ArticleList } from '@/mock/temp/articleList'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { ArticleType } from '@/api/modules'
|
||||
|
||||
defineOptions({ name: 'ArticleList' })
|
||||
|
||||
const yearVal = ref('All')
|
||||
|
||||
const options = ['All', '2024', '2023', '2022', '2021', '2020', '2019']
|
||||
|
||||
const searchVal = ref('')
|
||||
const articleList = ref<ArticleType[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(40)
|
||||
// const lastPage = ref(0)
|
||||
const total = ref(0)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const showEmpty = computed(() => {
|
||||
return articleList.value.length === 0 && !isLoading.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getArticleList({ backTop: false })
|
||||
})
|
||||
|
||||
// 搜索文章
|
||||
const searchArticle = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
// 根据年份查询文章
|
||||
const searchArticleByYear = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const getArticleList = async ({ backTop = false }) => {
|
||||
isLoading.value = true
|
||||
// let year = yearVal.value
|
||||
|
||||
if (searchVal.value) {
|
||||
yearVal.value = 'All'
|
||||
}
|
||||
|
||||
if (yearVal.value === 'All') {
|
||||
// year = ''
|
||||
}
|
||||
|
||||
// const params = {
|
||||
// page: currentPage.value,
|
||||
// size: pageSize.value,
|
||||
// searchVal: searchVal.value,
|
||||
// year
|
||||
// }
|
||||
|
||||
articleList.value = ArticleList
|
||||
isLoading.value = false
|
||||
|
||||
if (backTop) {
|
||||
useCommon().scrollToTop()
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleList(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// currentPage.value = res.currentPage
|
||||
// pageSize.value = res.pageSize
|
||||
// lastPage.value = res.lastPage
|
||||
// total.value = res.total
|
||||
// articleList.value = res.data
|
||||
|
||||
// // setTimeout(() => {
|
||||
// isLoading.value = false
|
||||
// // }, 3000)
|
||||
|
||||
// if (searchVal.value) {
|
||||
// searchVal.value = ''
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const toDetail = (item: ArticleType) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticleDetail,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toEdit = (item: ArticleType) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toAddArticle = () => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-list {
|
||||
.custom-segmented .el-segmented {
|
||||
height: 40px;
|
||||
padding: 6px;
|
||||
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 20px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
&:hover {
|
||||
.el-button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9.5;
|
||||
|
||||
.cover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: var(--art-gray-200);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px)
|
||||
calc(var(--custom-radius) / 2 + 2px) 0 0;
|
||||
|
||||
.image-slot {
|
||||
font-size: 26px;
|
||||
color: var(--art-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
padding: 5px 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(#fff, 0.8);
|
||||
background: rgba($color: #000, $alpha: 60%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 5px 10px;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
margin-top: 6px;
|
||||
line-height: 25px;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--art-text-gray-600);
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
margin: 0 15px;
|
||||
background-color: var(--art-border-dashed-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-notebook) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(25% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(33.333% - 20px);
|
||||
|
||||
.bottom {
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,411 +0,0 @@
|
||||
<template>
|
||||
<div class="article-edit">
|
||||
<div>
|
||||
<div class="editor-wrap">
|
||||
<!-- 文章标题、类型 -->
|
||||
<ElRow :gutter="10">
|
||||
<ElCol :span="18">
|
||||
<ElInput
|
||||
v-model.trim="articleName"
|
||||
placeholder="请输入文章标题(最多100个字符)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
|
||||
<ElOption
|
||||
v-for="item in articleTypes"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||
|
||||
<div class="form-wrap">
|
||||
<h2>发布设置</h2>
|
||||
<!-- 图片上传 -->
|
||||
<ElForm>
|
||||
<ElFormItem label="封面">
|
||||
<div class="el-top upload-container">
|
||||
<ElUpload
|
||||
class="cover-uploader"
|
||||
:action="uploadImageUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div v-if="!cover" class="upload-placeholder">
|
||||
<ElIcon class="upload-icon"><Plus /></ElIcon>
|
||||
<div class="upload-text">点击上传封面</div>
|
||||
</div>
|
||||
<img v-else :src="cover" class="cover-image" />
|
||||
</ElUpload>
|
||||
<div class="el-upload__tip">建议尺寸 16:9,jpg/png 格式</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="可见">
|
||||
<ElSwitch v-model="visible" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<ElButton type="primary" @click="submit" style="width: 100px">
|
||||
{{ pageMode === PageModeEnum.Edit ? '保存' : '发布' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="outline-wrap">
|
||||
<div class="item" v-for="(item, index) in outlineList" :key="index">
|
||||
<p :class="`level${item.level}`">{{ item.text }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ArticleService } from '@/api/articleApi'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { PageModeEnum } from '@/enums/formEnum'
|
||||
import axios from 'axios'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'ArticlePublish' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const userStore = useUserStore()
|
||||
let { accessToken } = userStore
|
||||
|
||||
// 上传路径
|
||||
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`
|
||||
// 传递 token
|
||||
const uploadHeaders = { Authorization: accessToken }
|
||||
|
||||
let pageMode: PageModeEnum = PageModeEnum.Add // 页面类型 新增 | 编辑
|
||||
const articleName = ref('') // 文章标题
|
||||
const articleType = ref() // 文章类型
|
||||
const articleTypes = ref() // 类型列表
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const createDate = ref('') // 创建时间
|
||||
const cover = ref('') // 图片
|
||||
const visible = ref(true) // 可见
|
||||
// const outlineList = ref()
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
getArticleTypes()
|
||||
initPageMode()
|
||||
})
|
||||
|
||||
// 初始化页面类型 新增 | 编辑
|
||||
const initPageMode = () => {
|
||||
const { id } = route.query
|
||||
pageMode = id ? PageModeEnum.Edit : PageModeEnum.Add
|
||||
if (pageMode === PageModeEnum.Edit && id) {
|
||||
initEditArticle(Number(id))
|
||||
} else {
|
||||
initAddArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑文章的逻辑
|
||||
const initEditArticle = (id: number) => {
|
||||
articleId = id
|
||||
getArticleDetail()
|
||||
}
|
||||
|
||||
// 初始化新增文章逻辑
|
||||
const initAddArticle = () => {
|
||||
createDate.value = formDate(useNow().value)
|
||||
}
|
||||
|
||||
// 获取文章类型
|
||||
const getArticleTypes = async () => {
|
||||
try {
|
||||
const response = await axios.get('https://www.qiniu.lingchen.kim/classify.json')
|
||||
if (response.data.code === 200) {
|
||||
articleTypes.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching JSON data:', error)
|
||||
}
|
||||
// try {
|
||||
// const res = await ArticleService.getArticleTypes({})
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTypes.value = res.data
|
||||
// }
|
||||
// } catch (err) { }
|
||||
}
|
||||
|
||||
// 获取文章详情内容
|
||||
let articleId: number = 0
|
||||
const getArticleDetail = async () => {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_list.json')
|
||||
|
||||
if (res.data.code === ApiStatus.success) {
|
||||
let { title, blog_class, html_content } = res.data.data
|
||||
articleName.value = title
|
||||
articleType.value = Number(blog_class)
|
||||
editorHtml.value = html_content
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleDetail(articleId)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// let { title, blog_class, create_time, home_img, html_content } = res.data
|
||||
|
||||
// articleName.value = title
|
||||
// articleType.value = Number(blog_class)
|
||||
// editorHtml.value = html_content
|
||||
// cover.value = home_img
|
||||
// createDate.value = formDate(create_time)
|
||||
|
||||
// // getOutline(html_content)
|
||||
// }
|
||||
}
|
||||
|
||||
// const getOutline = (content: string) => {
|
||||
// const regex = /<h([1-3])>(.*?)<\/h\1>/g
|
||||
// const headings = []
|
||||
// let match
|
||||
|
||||
// while ((match = regex.exec(content)) !== null) {
|
||||
// headings.push({ level: match[1], text: match[2] })
|
||||
// }
|
||||
// outlineList.value = headings
|
||||
// }
|
||||
|
||||
// 提交
|
||||
const submit = () => {
|
||||
if (pageMode === PageModeEnum.Edit) {
|
||||
editArticle()
|
||||
} else {
|
||||
addArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formDate = (date: string | Date): string => {
|
||||
return useDateFormat(date, 'YYYY-MM-DD').value
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
const validateArticle = () => {
|
||||
if (!articleName.value) {
|
||||
ElMessage.error(`请输入文章标题`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!articleType.value) {
|
||||
ElMessage.error(`请选择文章类型`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (editorHtml.value === '<p><br></p>') {
|
||||
ElMessage.error(`请输入文章内容`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!cover.value) {
|
||||
ElMessage.error(`请上传图片`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 构建参数
|
||||
const buildParams = () => {
|
||||
return {
|
||||
title: articleName.value,
|
||||
html_content: editorHtml.value,
|
||||
home_img: cover.value,
|
||||
blog_class: articleType.value,
|
||||
create_time: createDate.value
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文章
|
||||
const addArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
|
||||
const params = buildParams()
|
||||
const res = await ArticleService.addArticle(params)
|
||||
|
||||
if (res.code === ApiStatus.success) {
|
||||
ElMessage.success('发布成功')
|
||||
goBack()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
const editArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
|
||||
const params = buildParams()
|
||||
const res = await ArticleService.editArticle(articleId, params)
|
||||
|
||||
if (res.code === ApiStatus.success) {
|
||||
ElMessage.success('修改成功')
|
||||
goBack()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
const delCodeTrim = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
|
||||
const onSuccess = (response: any) => {
|
||||
cover.value = response.data.url
|
||||
ElMessage.success('图片上传成功')
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
ElMessage.error('图片上传失败')
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
setTimeout(() => {
|
||||
router.go(-1)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 添加上传前的校验
|
||||
const beforeUpload = (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-edit {
|
||||
.editor-wrap {
|
||||
max-width: 1000px;
|
||||
margin: 20px auto;
|
||||
|
||||
.el-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.outline-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 8px;
|
||||
|
||||
.item {
|
||||
p {
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.level3 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
.cover-uploader {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: var(--el-transition-duration);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
display: block;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3811,27 +3811,27 @@
|
||||
.wallet-balance-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 8px;
|
||||
|
||||
.balance-card {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 18px 20px;
|
||||
padding: 10px 14px;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 2px 6px rgb(0 0 0 / 6%);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.balance-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 32px;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -3839,17 +3839,17 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 2px;
|
||||
|
||||
.balance-label {
|
||||
font-size: 13px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
@@ -1,542 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和筛选区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="申请单号/ICCID" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="待处理" value="pending" />
|
||||
<ElOption label="处理中" value="processing" />
|
||||
<ElOption label="已完成" value="completed" />
|
||||
<ElOption label="已拒绝" value="rejected" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElDatePicker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="exportData">导出</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<ElRow :gutter="20" style="margin: 20px 0">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待处理</div>
|
||||
<div class="stat-value" style="color: var(--el-color-warning)">{{
|
||||
statistics.pending
|
||||
}}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><Clock /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">处理中</div>
|
||||
<div class="stat-value" style="color: var(--el-color-primary)">{{
|
||||
statistics.processing
|
||||
}}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Loading /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已完成</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">{{
|
||||
statistics.completed
|
||||
}}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"
|
||||
><CircleCheck
|
||||
/></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已拒绝</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">{{
|
||||
statistics.rejected
|
||||
}}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><CircleClose /></el-icon>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 换卡申请列表 -->
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="申请单号" prop="requestNo" width="180" />
|
||||
<ElTableColumn label="旧卡ICCID" prop="oldIccid" width="200" />
|
||||
<ElTableColumn label="申请人" prop="applicant" width="120" />
|
||||
<ElTableColumn label="联系电话" prop="phone" width="130" />
|
||||
<ElTableColumn label="换卡原因" prop="reason" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="新卡ICCID" prop="newIccid" width="200">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.newIccid" type="success" size="small">
|
||||
{{ scope.row.newIccid }}
|
||||
</ElTag>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">待填充</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'pending'" type="warning">待处理</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'processing'" type="primary">处理中</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
|
||||
<ElTag v-else type="danger">已拒绝</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="240">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleProcess(scope.row)"
|
||||
>
|
||||
处理
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'processing'"
|
||||
link
|
||||
type="success"
|
||||
@click="fillNewIccid(scope.row)"
|
||||
>
|
||||
填充新卡
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleReject(scope.row)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="换卡申请详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="申请单号">{{ currentRequest.requestNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请人">{{ currentRequest.applicant }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系电话">{{ currentRequest.phone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请时间">{{ currentRequest.applyTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="旧卡ICCID" :span="2">
|
||||
<ElTag type="warning">{{ currentRequest.oldIccid }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="新卡ICCID" :span="2">
|
||||
<ElTag v-if="currentRequest.newIccid" type="success">{{ currentRequest.newIccid }}</ElTag>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">待填充</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="换卡原因" :span="2">
|
||||
{{ currentRequest.reason }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="详细说明" :span="2">
|
||||
{{ currentRequest.description || '无' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag v-if="currentRequest.status === 'pending'" type="warning">待处理</ElTag>
|
||||
<ElTag v-else-if="currentRequest.status === 'processing'" type="primary">处理中</ElTag>
|
||||
<ElTag v-else-if="currentRequest.status === 'completed'" type="success">已完成</ElTag>
|
||||
<ElTag v-else type="danger">已拒绝</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="处理人">
|
||||
{{ currentRequest.processor || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="处理时间" :span="2">
|
||||
{{ currentRequest.processTime || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="currentRequest.rejectReason" label="拒绝原因" :span="2">
|
||||
<span style="color: var(--el-color-danger)">{{ currentRequest.rejectReason }}</span>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 填充新卡对话框 -->
|
||||
<ElDialog v-model="fillDialogVisible" title="填充新卡ICCID" width="500px" align-center>
|
||||
<ElForm ref="fillFormRef" :model="fillForm" :rules="fillRules" label-width="100px">
|
||||
<ElFormItem label="旧卡ICCID">
|
||||
<ElInput :value="currentRequest.oldIccid" disabled />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="新卡ICCID" prop="newIccid">
|
||||
<ElInput v-model="fillForm.newIccid" placeholder="请输入新卡ICCID" maxlength="20" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="验证新卡">
|
||||
<ElButton @click="validateNewIccid">验证ICCID</ElButton>
|
||||
<div v-if="validationResult" style="margin-top: 8px">
|
||||
<ElTag v-if="validationResult === 'success'" type="success" size="small">
|
||||
验证通过,该卡可用
|
||||
</ElTag>
|
||||
<ElTag v-else type="danger" size="small"> 验证失败,{{ validationMessage }} </ElTag>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="fillForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert type="info" :closable="false">
|
||||
填充新卡后,系统将自动完成换卡操作,旧卡将被停用
|
||||
</ElAlert>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="fillDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleFillSubmit">确认填充</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 拒绝对话框 -->
|
||||
<ElDialog v-model="rejectDialogVisible" title="拒绝换卡申请" width="500px" align-center>
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="100px">
|
||||
<ElFormItem label="拒绝原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="rejectForm.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入拒绝原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="danger" @click="handleRejectSubmit">确认拒绝</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
View,
|
||||
Clock,
|
||||
Loading as LoadingIcon,
|
||||
CircleCheck,
|
||||
CircleClose
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CardReplacementRequest' })
|
||||
|
||||
interface ReplacementRequest {
|
||||
id: string
|
||||
requestNo: string
|
||||
oldIccid: string
|
||||
newIccid?: string
|
||||
applicant: string
|
||||
phone: string
|
||||
reason: string
|
||||
description?: string
|
||||
status: 'pending' | 'processing' | 'completed' | 'rejected'
|
||||
applyTime: string
|
||||
processor?: string
|
||||
processTime?: string
|
||||
rejectReason?: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const detailDialogVisible = ref(false)
|
||||
const fillDialogVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const fillFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const validationResult = ref<string>('')
|
||||
const validationMessage = ref('')
|
||||
|
||||
const statistics = reactive({
|
||||
pending: 15,
|
||||
processing: 8,
|
||||
completed: 102,
|
||||
rejected: 5
|
||||
})
|
||||
|
||||
const fillForm = reactive({
|
||||
newIccid: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const fillRules = reactive<FormRules>({
|
||||
newIccid: [
|
||||
{ required: true, message: '请输入新卡ICCID', trigger: 'blur' },
|
||||
{ len: 20, message: 'ICCID长度必须为20位', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const rejectForm = reactive({
|
||||
reason: ''
|
||||
})
|
||||
|
||||
const rejectRules = reactive<FormRules>({
|
||||
reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const mockData = ref<ReplacementRequest[]>([
|
||||
{
|
||||
id: '1',
|
||||
requestNo: 'REP202601090001',
|
||||
oldIccid: '89860123456789012345',
|
||||
applicant: '张三',
|
||||
phone: '13800138000',
|
||||
reason: '卡片损坏',
|
||||
description: '卡片物理损坏无法使用',
|
||||
status: 'pending',
|
||||
applyTime: '2026-01-09 09:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
requestNo: 'REP202601080002',
|
||||
oldIccid: '89860123456789012346',
|
||||
newIccid: '89860123456789012350',
|
||||
applicant: '李四',
|
||||
phone: '13900139000',
|
||||
reason: '信号不稳定',
|
||||
description: '长期信号不稳定,影响使用',
|
||||
status: 'processing',
|
||||
applyTime: '2026-01-08 14:20:00',
|
||||
processor: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
requestNo: 'REP202601070003',
|
||||
oldIccid: '89860123456789012347',
|
||||
newIccid: '89860123456789012351',
|
||||
applicant: '王五',
|
||||
phone: '13700137000',
|
||||
reason: '卡片丢失',
|
||||
description: '卡片意外丢失',
|
||||
status: 'completed',
|
||||
applyTime: '2026-01-07 10:00:00',
|
||||
processor: 'admin',
|
||||
processTime: '2026-01-07 15:30:00'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
requestNo: 'REP202601060004',
|
||||
oldIccid: '89860123456789012348',
|
||||
applicant: '赵六',
|
||||
phone: '13600136000',
|
||||
reason: '套餐到期',
|
||||
description: '套餐到期需要换新卡',
|
||||
status: 'rejected',
|
||||
applyTime: '2026-01-06 11:00:00',
|
||||
processor: 'admin',
|
||||
processTime: '2026-01-06 12:00:00',
|
||||
rejectReason: '套餐到期应该续费而不是换卡'
|
||||
}
|
||||
])
|
||||
|
||||
const currentRequest = ref<ReplacementRequest>({
|
||||
id: '',
|
||||
requestNo: '',
|
||||
oldIccid: '',
|
||||
applicant: '',
|
||||
phone: '',
|
||||
reason: '',
|
||||
status: 'pending',
|
||||
applyTime: ''
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.requestNo.includes(searchQuery.value) ||
|
||||
item.oldIccid.includes(searchQuery.value) ||
|
||||
(item.newIccid && item.newIccid.includes(searchQuery.value))
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const exportData = () => {
|
||||
ElMessage.success('数据导出中...')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ReplacementRequest) => {
|
||||
currentRequest.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleProcess = (row: ReplacementRequest) => {
|
||||
ElMessageBox.confirm('确定要处理该换卡申请吗?', '处理确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}).then(() => {
|
||||
row.status = 'processing'
|
||||
row.processor = 'admin'
|
||||
ElMessage.success('已标记为处理中')
|
||||
})
|
||||
}
|
||||
|
||||
const fillNewIccid = (row: ReplacementRequest) => {
|
||||
currentRequest.value = { ...row }
|
||||
fillForm.newIccid = ''
|
||||
fillForm.remark = ''
|
||||
validationResult.value = ''
|
||||
fillDialogVisible.value = true
|
||||
}
|
||||
|
||||
const validateNewIccid = () => {
|
||||
if (!fillForm.newIccid) {
|
||||
ElMessage.warning('请先输入新卡ICCID')
|
||||
return
|
||||
}
|
||||
|
||||
if (fillForm.newIccid.length !== 20) {
|
||||
validationResult.value = 'error'
|
||||
validationMessage.value = 'ICCID长度必须为20位'
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟验证
|
||||
setTimeout(() => {
|
||||
const exists = mockData.value.some((item) => item.oldIccid === fillForm.newIccid)
|
||||
if (exists) {
|
||||
validationResult.value = 'error'
|
||||
validationMessage.value = '该ICCID已被使用'
|
||||
} else {
|
||||
validationResult.value = 'success'
|
||||
validationMessage.value = ''
|
||||
ElMessage.success('验证通过')
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleFillSubmit = async () => {
|
||||
if (!fillFormRef.value) return
|
||||
|
||||
if (validationResult.value !== 'success') {
|
||||
ElMessage.warning('请先验证新卡ICCID')
|
||||
return
|
||||
}
|
||||
|
||||
await fillFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const index = mockData.value.findIndex((item) => item.id === currentRequest.value.id)
|
||||
if (index !== -1) {
|
||||
mockData.value[index].newIccid = fillForm.newIccid
|
||||
mockData.value[index].status = 'completed'
|
||||
mockData.value[index].processTime = new Date().toLocaleString('zh-CN')
|
||||
|
||||
statistics.processing--
|
||||
statistics.completed++
|
||||
}
|
||||
|
||||
fillDialogVisible.value = false
|
||||
ElMessage.success('新卡填充成功,换卡操作已完成')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleReject = (row: ReplacementRequest) => {
|
||||
currentRequest.value = { ...row }
|
||||
rejectForm.reason = ''
|
||||
rejectDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleRejectSubmit = async () => {
|
||||
if (!rejectFormRef.value) return
|
||||
await rejectFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const index = mockData.value.findIndex((item) => item.id === currentRequest.value.id)
|
||||
if (index !== -1) {
|
||||
mockData.value[index].status = 'rejected'
|
||||
mockData.value[index].rejectReason = rejectForm.reason
|
||||
mockData.value[index].processor = 'admin'
|
||||
mockData.value[index].processTime = new Date().toLocaleString('zh-CN')
|
||||
|
||||
statistics.pending--
|
||||
statistics.rejected++
|
||||
}
|
||||
|
||||
rejectDialogVisible.value = false
|
||||
ElMessage.success('已拒绝该换卡申请')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -439,46 +439,6 @@
|
||||
</div>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 设置限速对话框 -->
|
||||
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="50%">
|
||||
<ElForm
|
||||
ref="speedLimitFormRef"
|
||||
:model="speedLimitForm"
|
||||
:rules="speedLimitRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="设备号">
|
||||
<span style="font-weight: bold; color: #409eff">{{ currentOperatingImei }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="下行速率" prop="download_speed">
|
||||
<ElInputNumber
|
||||
v-model="speedLimitForm.download_speed"
|
||||
:min="1"
|
||||
:step="128"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
v-model="speedLimitForm.upload_speed"
|
||||
:min="1"
|
||||
:step="128"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: #909399">单位: KB/s</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="speedLimitDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirmSpeedLimit" :loading="speedLimitLoading">
|
||||
确认设置
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 切换SIM卡对话框 -->
|
||||
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
|
||||
<div v-if="loadingDeviceCards" style="padding: 40px; text-align: center">
|
||||
@@ -604,6 +564,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import {
|
||||
DeviceService,
|
||||
ShopService,
|
||||
@@ -615,10 +576,8 @@
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElSwitch,
|
||||
ElIcon,
|
||||
ElTreeSelect,
|
||||
ElInputNumber,
|
||||
ElRadioGroup,
|
||||
ElRadio
|
||||
} from 'element-plus'
|
||||
@@ -634,9 +593,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'DeviceList' })
|
||||
@@ -694,23 +651,6 @@
|
||||
})
|
||||
|
||||
// 设备操作相关对话框
|
||||
const speedLimitDialogVisible = ref(false)
|
||||
const speedLimitLoading = ref(false)
|
||||
const speedLimitFormRef = ref<FormInstance>()
|
||||
const speedLimitForm = reactive({
|
||||
download_speed: 1024,
|
||||
upload_speed: 512
|
||||
})
|
||||
const speedLimitRules = reactive<FormRules>({
|
||||
download_speed: [
|
||||
{ required: true, message: '请输入下行速率', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
|
||||
],
|
||||
upload_speed: [
|
||||
{ required: true, message: '请输入上行速率', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const currentOperatingImei = ref<string>('') // 用于存储当前操作设备的IMEI
|
||||
const currentOperatingDevice = ref<Device | null>(null)
|
||||
@@ -872,7 +812,7 @@
|
||||
const goToDeviceSearchDetail = (deviceNo: string) => {
|
||||
if (hasAuth('device:view_detail')) {
|
||||
router.push({
|
||||
path: '/asset-management/asset-information',
|
||||
path: RoutesAlias.AssetInformation,
|
||||
query: {
|
||||
virtual_no: deviceNo
|
||||
}
|
||||
@@ -975,7 +915,7 @@
|
||||
// 重置表单
|
||||
bindCardForm.iot_card_id = undefined
|
||||
bindCardForm.slot_position = 1
|
||||
bindCardFormRef.value.resetFields()
|
||||
bindCardFormRef.value?.resetFields()
|
||||
// 重新加载卡列表
|
||||
await loadDeviceCards(currentDeviceDetail.value.virtual_no)
|
||||
// 刷新设备详情以更新绑定卡数量
|
||||
@@ -1016,7 +956,7 @@
|
||||
currentDeviceDetail.value = detailRes.data
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '解绑失败')
|
||||
ElMessage.error(res.msg || '解绑失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('解绑卡失败:', error)
|
||||
@@ -1027,28 +967,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 获取卡状态标签类型
|
||||
const getCardStatusTagType = (status: number) => {
|
||||
const typeMap: Record<number, any> = {
|
||||
1: 'info', // 在库
|
||||
2: 'warning', // 已分销
|
||||
3: 'success', // 已激活
|
||||
4: 'danger' // 已停用
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取卡状态文本
|
||||
const getCardStatusText = (status: number) => {
|
||||
const textMap: Record<number, string> = {
|
||||
1: '在库',
|
||||
2: '已分销',
|
||||
3: '已激活',
|
||||
4: '已停用'
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取网络状态标签类型
|
||||
const getNetworkStatusTagType = (networkStatus: number) => {
|
||||
// 0: 停机 (danger 红色)
|
||||
@@ -1581,22 +1499,13 @@
|
||||
handleResetDevice(imei)
|
||||
break
|
||||
}
|
||||
case 'speed-limit': {
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行限速操作')
|
||||
return
|
||||
}
|
||||
showSpeedLimitDialog(imei)
|
||||
break
|
||||
}
|
||||
case 'switch-card': {
|
||||
const imei = device?.imei || (await getDeviceImei(deviceNo))
|
||||
if (!imei) {
|
||||
ElMessage.error('无法获取设备IMEI,无法执行切卡操作')
|
||||
return
|
||||
}
|
||||
showSwitchCardDialog(imei)
|
||||
await showSwitchCardDialog(imei)
|
||||
break
|
||||
}
|
||||
case 'set-wifi': {
|
||||
@@ -1612,7 +1521,7 @@
|
||||
handleManualDeactivateDevice()
|
||||
break
|
||||
case 'delete':
|
||||
handleDeleteDeviceByNo(deviceNo)
|
||||
await handleDeleteDeviceByNo(deviceNo)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1649,7 +1558,7 @@
|
||||
const res = await AssetService.deactivateAsset(device.virtual_no)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
loadDeviceList()
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('手动停用失败:', error)
|
||||
@@ -1684,7 +1593,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('重启指令已发送')
|
||||
} else {
|
||||
ElMessage.error(res.message || '重启失败')
|
||||
ElMessage.error(res.msg || '重启失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('重启设备失败:', error)
|
||||
@@ -1712,7 +1621,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('恢复出厂设置指令已发送')
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('恢复出厂设置失败:', error)
|
||||
@@ -1723,41 +1632,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 显示设置限速对话框
|
||||
const showSpeedLimitDialog = (imei: string) => {
|
||||
currentOperatingImei.value = imei
|
||||
speedLimitForm.download_speed = 1024
|
||||
speedLimitForm.upload_speed = 512
|
||||
speedLimitDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 确认设置限速
|
||||
const handleConfirmSpeedLimit = async () => {
|
||||
if (!speedLimitFormRef.value) return
|
||||
|
||||
await speedLimitFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
speedLimitLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.setSpeedLimit(currentOperatingImei.value, {
|
||||
download_speed: speedLimitForm.download_speed,
|
||||
upload_speed: speedLimitForm.upload_speed
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('限速设置成功')
|
||||
speedLimitDialogVisible.value = false
|
||||
} else {
|
||||
ElMessage.error(res.message || '设置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('设置限速失败:', error)
|
||||
} finally {
|
||||
speedLimitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示切换SIM卡对话框
|
||||
const showSwitchCardDialog = async (imei: string) => {
|
||||
currentOperatingImei.value = imei
|
||||
@@ -1812,7 +1686,7 @@
|
||||
ElMessage.success('切换SIM卡指令已发送')
|
||||
switchCardDialogVisible.value = false
|
||||
} else {
|
||||
ElMessage.error(res.message || '切换失败')
|
||||
ElMessage.error(res.msg || '切换失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('切换SIM卡失败:', error)
|
||||
@@ -1849,7 +1723,7 @@
|
||||
ElMessage.success('WiFi设置成功')
|
||||
setWiFiDialogVisible.value = false
|
||||
} else {
|
||||
ElMessage.error(res.message || '设置失败')
|
||||
ElMessage.error(res.msg || '设置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('设置WiFi失败:', error)
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
<template>
|
||||
<div class="device-search-page">
|
||||
<!-- 搜索区域 -->
|
||||
<ElCard shadow="never" class="search-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>设备查询</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-content">
|
||||
<ElForm :model="searchForm" label-width="100px">
|
||||
<ElFormItem label="设备号(IMEI)">
|
||||
<ElInput
|
||||
v-model="searchForm.imei"
|
||||
placeholder="请输入设备号(IMEI)"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 设备详情区域 -->
|
||||
<ElCard v-if="deviceDetail" shadow="never" class="detail-card" style="margin-top: 16px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>设备详情</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptionsItem label="设备ID">{{ deviceDetail.id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备号" :span="2">{{
|
||||
deviceDetail.virtual_no
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="设备名称">{{
|
||||
deviceDetail.device_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备型号">{{
|
||||
deviceDetail.device_model || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="设备类型">{{
|
||||
deviceDetail.device_type || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="制造商">{{
|
||||
deviceDetail.manufacturer || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最大插槽数">{{ deviceDetail.max_sim_slots }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已绑定卡数量">{{
|
||||
deviceDetail.bound_card_count
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusTagType(deviceDetail.status)">
|
||||
{{ deviceDetail.status_name }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="在线状态">
|
||||
<ElTag :type="getOnlineStatusTagType(deviceDetail.online_status)">
|
||||
{{ getOnlineStatusText(deviceDetail.online_status) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺名称">{{
|
||||
deviceDetail.shop_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="固件版本">{{
|
||||
deviceDetail.software_version || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="切卡模式">{{
|
||||
getSwitchModeText(deviceDetail.switch_mode)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="最后在线时间">{{
|
||||
deviceDetail.last_online_time || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后同步时间">{{
|
||||
deviceDetail.last_gateway_sync_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="激活时间">{{
|
||||
deviceDetail.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem></ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ElEmpty v-if="searched && !deviceDetail && !loading" description="未找到相关设备信息" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AssetService } from '@/api/modules'
|
||||
|
||||
defineOptions({ name: 'DeviceSearch' })
|
||||
|
||||
const loading = ref(false)
|
||||
const searched = ref(false)
|
||||
const searchForm = reactive({
|
||||
imei: ''
|
||||
})
|
||||
const deviceDetail = ref<any>(null)
|
||||
|
||||
// 查询设备详情
|
||||
const handleSearch = async () => {
|
||||
if (!searchForm.imei.trim()) {
|
||||
ElMessage.warning('请输入设备号(IMEI)')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
searched.value = true
|
||||
deviceDetail.value = null
|
||||
|
||||
try {
|
||||
const res = await AssetService.resolveAsset(searchForm.imei.trim())
|
||||
if (res.code === 0 && res.data) {
|
||||
deviceDetail.value = res.data
|
||||
ElMessage.success('查询成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '查询失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询设备详情失败:', error)
|
||||
console.log(error?.message || '查询失败,请检查设备号是否正确')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status: number) => {
|
||||
const typeMap: Record<number, any> = {
|
||||
1: 'info', // 在库
|
||||
2: 'warning', // 已分销
|
||||
3: 'success', // 已激活
|
||||
4: 'danger' // 已停用
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取在线状态标签类型
|
||||
const getOnlineStatusTagType = (status?: number) => {
|
||||
const typeMap: Record<number, any> = {
|
||||
0: 'info', // 未知
|
||||
1: 'success', // 在线
|
||||
2: 'danger' // 离线
|
||||
}
|
||||
return typeMap[status ?? 0] || 'info'
|
||||
}
|
||||
|
||||
// 获取在线状态文本
|
||||
const getOnlineStatusText = (status?: number) => {
|
||||
const textMap: Record<number, string> = {
|
||||
0: '未知',
|
||||
1: '在线',
|
||||
2: '离线'
|
||||
}
|
||||
return textMap[status ?? 0] || '未知'
|
||||
}
|
||||
|
||||
// 获取切卡模式文本
|
||||
const getSwitchModeText = (mode?: string) => {
|
||||
const modeMap: Record<string, string> = {
|
||||
'0': '自动',
|
||||
'1': '手动'
|
||||
}
|
||||
return modeMap[mode || ''] || '--'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-search-page {
|
||||
padding: 16px;
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.search-content {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,28 +31,16 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 创建换货单对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
@@ -217,25 +205,15 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
defineOptions({ name: 'ExchangeManagement' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
// 右键菜单相关
|
||||
const { showContextMenuHint, hintPosition, handleCellMouseEnter, handleCellMouseLeave } =
|
||||
useTableContextMenu()
|
||||
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentExchangeRow = ref<ExchangeResponse | null>(null)
|
||||
|
||||
const loading = ref(false)
|
||||
const shipDialogVisible = ref(false)
|
||||
const shipLoading = ref(false)
|
||||
@@ -621,17 +599,18 @@
|
||||
router.push(`${RoutesAlias.ExchangeDetail}/${row.id}`)
|
||||
}
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
const items: MenuItemType[] = []
|
||||
const status = currentExchangeRow.value?.status
|
||||
// 获取操作按钮
|
||||
const getActions = (row: ExchangeResponse) => {
|
||||
const actions: any[] = []
|
||||
const status = row.status
|
||||
|
||||
// 状态1(待填写信息):取消换货
|
||||
if (status === 1) {
|
||||
if (hasAuth('exchange:cancel')) {
|
||||
items.push({
|
||||
key: 'cancel',
|
||||
label: '取消换货'
|
||||
actions.push({
|
||||
label: '取消换货',
|
||||
handler: () => handleCancelExchange(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -639,15 +618,17 @@
|
||||
// 状态2(待发货):发货、取消换货
|
||||
if (status === 2) {
|
||||
if (hasAuth('exchange:ship')) {
|
||||
items.push({
|
||||
key: 'ship',
|
||||
label: '发货'
|
||||
actions.push({
|
||||
label: '发货',
|
||||
handler: () => handleShipExchange(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth('exchange:cancel')) {
|
||||
items.push({
|
||||
key: 'cancel',
|
||||
label: '取消换货'
|
||||
actions.push({
|
||||
label: '取消换货',
|
||||
handler: () => handleCancelExchange(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -655,9 +636,10 @@
|
||||
// 状态3(已发货待确认):确认完成
|
||||
if (status === 3) {
|
||||
if (hasAuth('exchange:complete')) {
|
||||
items.push({
|
||||
key: 'complete',
|
||||
label: '确认完成'
|
||||
actions.push({
|
||||
label: '确认完成',
|
||||
handler: () => handleCompleteExchange(row),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -665,42 +647,15 @@
|
||||
// 状态4(已完成):旧资产转新
|
||||
if (status === 4) {
|
||||
if (hasAuth('exchange:renew')) {
|
||||
items.push({
|
||||
key: 'renew',
|
||||
label: '旧资产转新'
|
||||
actions.push({
|
||||
label: '旧资产转新',
|
||||
handler: () => handleRenewExchange(row),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理右键菜单
|
||||
const handleRowContextMenu = (row: ExchangeResponse, _column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentExchangeRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentExchangeRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'cancel':
|
||||
handleCancelExchange(currentExchangeRow.value)
|
||||
break
|
||||
case 'ship':
|
||||
handleShipExchange(currentExchangeRow.value)
|
||||
break
|
||||
case 'complete':
|
||||
handleCompleteExchange(currentExchangeRow.value)
|
||||
break
|
||||
case 'renew':
|
||||
handleRenewExchange(currentExchangeRow.value)
|
||||
break
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// 取消换货
|
||||
|
||||
@@ -572,6 +572,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { CardService, ShopService, PackageSeriesService, AssetService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
@@ -906,7 +907,7 @@
|
||||
const goToCardDetail = (iccid: string) => {
|
||||
if (hasAuth('iot_card:view_detail')) {
|
||||
router.push({
|
||||
path: '/asset-management/asset-information',
|
||||
path: RoutesAlias.AssetInformation,
|
||||
query: {
|
||||
iccid: iccid
|
||||
}
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<div class="card-search-page">
|
||||
<!-- 搜索区域 -->
|
||||
<ElCard shadow="never" class="search-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>单卡查询</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-content">
|
||||
<ElForm :model="searchForm" label-width="100px">
|
||||
<ElFormItem label="ICCID">
|
||||
<ElInput
|
||||
v-model="searchForm.iccid"
|
||||
placeholder="请输入ICCID"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 卡片详情区域 -->
|
||||
<ElCard v-if="cardDetail" shadow="never" class="detail-card" style="margin-top: 16px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>卡片详情</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptionsItem label="卡ID">{{ cardDetail.id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="ICCID" :span="2">{{ cardDetail.iccid }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="IMSI">{{ cardDetail.imsi || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡接入号">{{ cardDetail.msisdn || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商">{{ cardDetail.carrier_name }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="运营商类型">{{
|
||||
getCarrierTypeText(cardDetail.carrier_type)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡类型">{{ cardDetail.card_type }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="卡业务类型">{{
|
||||
getCardCategoryText(cardDetail.card_category)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusTagType(cardDetail.status)">
|
||||
{{ getStatusText(cardDetail.status) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="激活状态">
|
||||
<ElTag :type="cardDetail.activation_status === 1 ? 'success' : 'info'">
|
||||
{{ cardDetail.activation_status === 1 ? '已激活' : '未激活' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="网络状态">
|
||||
<ElTag :type="cardDetail.network_status === 1 ? 'success' : 'danger'">
|
||||
{{ cardDetail.network_status === 1 ? '开机' : '停机' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="实名状态">
|
||||
<ElTag :type="cardDetail.real_name_status === 1 ? 'success' : 'warning'">
|
||||
{{ cardDetail.real_name_status === 1 ? '已实名' : '未实名' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{ cardDetail.batch_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="供应商">{{ cardDetail.supplier || '--' }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="店铺名称">{{ cardDetail.shop_name || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成本价">{{
|
||||
formatPrice(cardDetail.cost_price)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="分销价">{{
|
||||
formatPrice(cardDetail.distribute_price)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="累计流量使用"
|
||||
>{{ cardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
||||
>
|
||||
<ElDescriptionsItem label="激活时间">{{
|
||||
cardDetail.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ cardDetail.created_at }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ElEmpty v-if="searched && !cardDetail && !loading" description="未找到相关卡片信息" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CardService } from '@/api/modules/card'
|
||||
|
||||
defineOptions({ name: 'CardSearch' })
|
||||
|
||||
const loading = ref(false)
|
||||
const searched = ref(false)
|
||||
const searchForm = reactive({
|
||||
iccid: ''
|
||||
})
|
||||
const cardDetail = ref<any>(null)
|
||||
|
||||
// 查询卡片详情
|
||||
const handleSearch = async () => {
|
||||
if (!searchForm.iccid.trim()) {
|
||||
ElMessage.warning('请输入ICCID')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
searched.value = true
|
||||
cardDetail.value = null
|
||||
|
||||
try {
|
||||
const res = await CardService.getIotCardDetailByIccid(searchForm.iccid.trim())
|
||||
if (res.code === 0 && res.data) {
|
||||
cardDetail.value = res.data
|
||||
ElMessage.success('查询成功')
|
||||
} else {
|
||||
ElMessage.error(res.message || '查询失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询卡片详情失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取运营商类型文本
|
||||
const getCarrierTypeText = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信',
|
||||
CBN: '中国广电'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取卡业务类型文本
|
||||
const getCardCategoryText = (category: string) => {
|
||||
const categoryMap: Record<string, string> = {
|
||||
normal: '普通卡',
|
||||
industry: '行业卡'
|
||||
}
|
||||
return categoryMap[category] || category
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: number) => {
|
||||
const statusMap: Record<number, string> = {
|
||||
1: '在库',
|
||||
2: '已分销',
|
||||
3: '已激活',
|
||||
4: '已停用'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status: number) => {
|
||||
const typeMap: Record<number, any> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化价格(分转元)
|
||||
const formatPrice = (price: number) => {
|
||||
return `¥${(price / 100).toFixed(2)}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-search-page {
|
||||
padding: 16px;
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.search-content {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -39,28 +39,15 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +111,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||
@@ -131,12 +119,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -146,28 +129,18 @@
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const fileList = ref<File[]>([])
|
||||
const uploading = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<DeviceImportTask | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
status: undefined,
|
||||
batch_no: '',
|
||||
dateRange: undefined as any,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
}
|
||||
@@ -560,7 +533,8 @@
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
@@ -579,13 +553,14 @@
|
||||
})
|
||||
|
||||
if (importRes.code !== 0) {
|
||||
throw new Error(importRes.msg || '创建导入任务失败')
|
||||
ElMessage.error(importRes.msg || '创建导入任务失败')
|
||||
return
|
||||
}
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
|
||||
handleCancelImport()
|
||||
getTableData()
|
||||
await getTableData()
|
||||
|
||||
ElMessage.success({
|
||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||
@@ -594,6 +569,7 @@
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('设备导入失败:', error)
|
||||
ElMessage.error(error.message || '设备导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
@@ -646,36 +622,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
if (!currentRow.value) return []
|
||||
// 获取操作按钮
|
||||
const getActions = (row: DeviceImportTask) => {
|
||||
const actions: any[] = []
|
||||
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (currentRow.value.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
|
||||
items.push({ key: 'failData', label: '失败数据' })
|
||||
if (row.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
|
||||
actions.push({
|
||||
label: '失败数据',
|
||||
handler: () => downloadFailDataByRow(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: DeviceImportTask, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'failData':
|
||||
downloadFailDataByRow(currentRow.value)
|
||||
break
|
||||
}
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -697,8 +656,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -39,28 +39,15 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +172,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { CardService, CarrierService } from '@/api/modules'
|
||||
import {
|
||||
ElMessage,
|
||||
@@ -200,11 +188,7 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
||||
@@ -216,15 +200,6 @@
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
@@ -234,8 +209,6 @@
|
||||
const selectedCarrierId = ref<number>()
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const carrierLoading = ref(false)
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<IotCardImportTask | null>(null)
|
||||
const failDataDialogVisible = ref(false)
|
||||
const failDataLoading = ref(false)
|
||||
const failedItems = ref<any[]>([])
|
||||
@@ -280,7 +253,7 @@
|
||||
label: '运营商',
|
||||
prop: 'carrier_id',
|
||||
type: 'select',
|
||||
options: carrierList.value.map((carrier) => ({
|
||||
options: carrierList.value.map((carrier: Carrier) => ({
|
||||
label: carrier.carrier_name,
|
||||
value: carrier.id
|
||||
})),
|
||||
@@ -738,7 +711,8 @@
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
@@ -758,7 +732,8 @@
|
||||
})
|
||||
|
||||
if (importRes.code !== 0) {
|
||||
throw new Error(importRes.msg || '创建导入任务失败')
|
||||
ElMessage.error(importRes.msg || '创建导入任务失败')
|
||||
return
|
||||
}
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
@@ -773,41 +748,25 @@
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('IoT卡导入失败:', error)
|
||||
ElMessage.error(error.message || 'IoT卡导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
if (!currentRow.value) return []
|
||||
// 获取操作按钮
|
||||
const getActions = (row: IotCardImportTask) => {
|
||||
const actions: any[] = []
|
||||
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (currentRow.value.fail_count > 0 && hasAuth('iot_card_task:download_fail_data')) {
|
||||
items.push({ key: 'failData', label: '失败数据' })
|
||||
if (row.fail_count > 0 && hasAuth('iot_card_task:download_fail_data')) {
|
||||
actions.push({
|
||||
label: '失败数据',
|
||||
handler: () => showFailDataDialog(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: IotCardImportTask, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'failData':
|
||||
showFailDataDialog(currentRow.value)
|
||||
break
|
||||
}
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -829,8 +788,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -47,16 +47,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { computed, h } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { CardService, DeviceService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElDivider, ElTable, ElTableColumn, ElIcon } from 'element-plus'
|
||||
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
|
||||
defineOptions({ name: 'TaskDetail' })
|
||||
|
||||
@@ -89,11 +90,11 @@
|
||||
|
||||
// 详情页配置
|
||||
const detailSections = computed((): DetailSection[] => {
|
||||
const sections: DetailSection[] = [
|
||||
return [
|
||||
{
|
||||
title: '任务基本信息',
|
||||
fields: [
|
||||
{ label: '任务编号', prop: 'task_no', formatter: (value) => value || '-' },
|
||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '任务类型',
|
||||
render: () => {
|
||||
@@ -104,8 +105,8 @@
|
||||
)
|
||||
}
|
||||
},
|
||||
{ label: '批次号', prop: 'batch_no', formatter: (value) => value || '-' },
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value) => value || '-' },
|
||||
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '任务状态',
|
||||
render: (data: TaskDetail) => {
|
||||
@@ -124,17 +125,17 @@
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '开始处理时间',
|
||||
prop: 'started_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value) => (value ? formatDateTime(value) : '-')
|
||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
...(taskDetail.value?.error_message
|
||||
? [
|
||||
@@ -195,8 +196,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return sections
|
||||
})
|
||||
|
||||
// 返回列表
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div class="login register">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
<div class="right-wrap">
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('forgetPassword.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
|
||||
<div class="input-wrap">
|
||||
<span class="input-label" v-if="showInputLabel">账号</span>
|
||||
<ElInput :placeholder="$t('forgetPassword.placeholder')" v-model.trim="username" />
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="login-btn"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('forgetPassword.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton class="back-btn" plain @click="toLogin">
|
||||
{{ $t('forgetPassword.backBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' })
|
||||
|
||||
const router = useRouter()
|
||||
const showInputLabel = ref(false)
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const username = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const register = async () => {}
|
||||
|
||||
const toLogin = () => {
|
||||
router.push(RoutesAlias.Login)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../login/index';
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
.register {
|
||||
.right-wrap {
|
||||
.login-wrap {
|
||||
.form {
|
||||
.el-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.privacy-policy {
|
||||
margin-top: 15px;
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<div class="login register">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
<div class="right-wrap">
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('register.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('register.subTitle') }}</p>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-position="top">
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
v-model.trim="formData.username"
|
||||
:placeholder="$t('register.placeholder[0]')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model.trim="formData.password"
|
||||
:placeholder="$t('register.placeholder[1]')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model.trim="formData.confirmPassword"
|
||||
:placeholder="$t('register.placeholder[2]')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@keyup.enter="register"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="agreement">
|
||||
<ElCheckbox v-model="formData.agreement">
|
||||
{{ $t('register.agreeText') }}
|
||||
<router-link
|
||||
style="color: var(--main-color); text-decoration: none"
|
||||
to="/privacy-policy"
|
||||
>{{ $t('register.privacyPolicy') }}</router-link
|
||||
>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="register-btn"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('register.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
{{ $t('register.hasAccount') }}
|
||||
<router-link :to="RoutesAlias.Login">{{ $t('register.toLogin') }}</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'Register' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreement: false
|
||||
})
|
||||
|
||||
const validatePass = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('register.placeholder[1]')))
|
||||
} else {
|
||||
if (formData.confirmPassword !== '') {
|
||||
formRef.value?.validateField('confirmPassword')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validatePass2 = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('register.rule[0]')))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error(t('register.rule[1]')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: t('register.placeholder[0]'), trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: t('register.rule[2]'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, validator: validatePass, trigger: 'blur' },
|
||||
{ min: 6, message: t('register.rule[3]'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [{ required: true, validator: validatePass2, trigger: 'blur' }],
|
||||
agreement: [
|
||||
{
|
||||
validator: (rule: any, value: boolean, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule[4]')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const register = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// 模拟注册请求
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
ElMessage.success('注册成功')
|
||||
toLogin()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.log('验证失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toLogin = () => {
|
||||
setTimeout(() => {
|
||||
router.push(RoutesAlias.Login)
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../login/index' as login;
|
||||
@use './index' as register;
|
||||
</style>
|
||||
@@ -1,570 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和操作区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="通知标题/内容" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="待发送" value="pending" />
|
||||
<ElOption label="发送中" value="sending" />
|
||||
<ElOption label="已发送" value="sent" />
|
||||
<ElOption label="发送失败" value="failed" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="typeFilter" placeholder="通知类型" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="卡片更换" value="replace" />
|
||||
<ElOption label="卡片激活" value="activate" />
|
||||
<ElOption label="卡片停用" value="deactivate" />
|
||||
<ElOption label="套餐变更" value="plan_change" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple type="primary" @click="showDialog('add')">新增通知</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="通知标题" prop="title" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="通知类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTypeTagType(scope.row.type)">
|
||||
{{ getTypeText(scope.row.type) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="目标用户数" prop="targetCount" width="120">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.targetCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已发送" prop="sentCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.sentCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="发送进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="
|
||||
scope.row.status === 'failed'
|
||||
? 'exception'
|
||||
: scope.row.status === 'sent'
|
||||
? 'success'
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'pending'" type="info">待发送</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'sending'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
发送中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'sent'" type="success">已发送</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">发送失败</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="发送方式" prop="sendMethod" width="120">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; gap: 4px">
|
||||
<ElTag v-for="method in scope.row.sendMethods" :key="method" size="small">
|
||||
{{ getSendMethodText(method) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn label="发送时间" prop="sendTime" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.sendTime || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="240">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Promotion"
|
||||
@click="handleSend(scope.row)"
|
||||
>
|
||||
立即发送
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending'"
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'pending' || scope.row.status === 'failed'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑通知对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增换卡通知' : '编辑换卡通知'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="通知标题" prop="title">
|
||||
<ElInput
|
||||
v-model="form.title"
|
||||
placeholder="请输入通知标题"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知类型" prop="type">
|
||||
<ElRadioGroup v-model="form.type">
|
||||
<ElRadio value="replace">卡片更换</ElRadio>
|
||||
<ElRadio value="activate">卡片激活</ElRadio>
|
||||
<ElRadio value="deactivate">卡片停用</ElRadio>
|
||||
<ElRadio value="plan_change">套餐变更</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知内容" prop="content">
|
||||
<ElInput
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入通知内容"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标用户" prop="targetType">
|
||||
<ElRadioGroup v-model="form.targetType">
|
||||
<ElRadio value="all">全部用户</ElRadio>
|
||||
<ElRadio value="specific">指定用户</ElRadio>
|
||||
<ElRadio value="batch">批量导入</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.targetType === 'specific'" label="用户列表">
|
||||
<ElSelect
|
||||
v-model="form.targetUsers"
|
||||
multiple
|
||||
placeholder="请选择目标用户"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="张三 (13800138000)" value="user1" />
|
||||
<ElOption label="李四 (13900139000)" value="user2" />
|
||||
<ElOption label="王五 (13700137000)" value="user3" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.targetType === 'batch'" label="用户文件">
|
||||
<ElUpload
|
||||
:action="uploadUrl"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
accept=".xlsx,.xls,.txt"
|
||||
>
|
||||
<ElButton type="primary">选择文件</ElButton>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持 Excel 或 TXT 格式,每行一个用户手机号</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="发送方式" prop="sendMethods">
|
||||
<ElCheckboxGroup v-model="form.sendMethods">
|
||||
<ElCheckbox value="sms">短信</ElCheckbox>
|
||||
<ElCheckbox value="email">邮件</ElCheckbox>
|
||||
<ElCheckbox value="app">App推送</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="定时发送">
|
||||
<ElSwitch v-model="form.scheduleSend" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
{{ form.scheduleSend ? '启用定时发送' : '立即发送' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.scheduleSend" label="发送时间" prop="scheduleTime">
|
||||
<ElDatePicker
|
||||
v-model="form.scheduleTime"
|
||||
type="datetime"
|
||||
placeholder="选择发送时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">
|
||||
{{ form.scheduleSend ? '创建定时任务' : '保存' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="通知详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="通知标题">{{ currentDetail.title }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="通知类型">
|
||||
<ElTag :type="getTypeTagType(currentDetail.type)">
|
||||
{{ getTypeText(currentDetail.type) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="目标用户数">{{ currentDetail.targetCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已发送数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.sentCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送状态">
|
||||
<ElTag v-if="currentDetail.status === 'sent'" type="success">已发送</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'sending'" type="warning">发送中</ElTag>
|
||||
<ElTag v-else-if="currentDetail.status === 'failed'" type="danger">发送失败</ElTag>
|
||||
<ElTag v-else type="info">待发送</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送方式" :span="2">
|
||||
<ElTag
|
||||
v-for="method in currentDetail.sendMethods"
|
||||
:key="method"
|
||||
size="small"
|
||||
style="margin-right: 4px"
|
||||
>
|
||||
{{ getSendMethodText(method) }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="发送时间">{{
|
||||
currentDetail.sendTime || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="通知内容" :span="2">
|
||||
{{ currentDetail.content }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Loading, Promotion } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadProps } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CardChangeNotice' })
|
||||
|
||||
interface Notice {
|
||||
id: string
|
||||
title: string
|
||||
type: 'replace' | 'activate' | 'deactivate' | 'plan_change'
|
||||
content: string
|
||||
targetCount: number
|
||||
sentCount: number
|
||||
failCount: number
|
||||
progress: number
|
||||
status: 'pending' | 'sending' | 'sent' | 'failed'
|
||||
sendMethods: string[]
|
||||
createTime: string
|
||||
sendTime?: string
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadUrl = ref('/api/batch/upload-users')
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetType: 'all',
|
||||
targetUsers: [] as string[],
|
||||
sendMethods: ['sms'],
|
||||
scheduleSend: false,
|
||||
scheduleTime: null as Date | null
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入通知标题', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择通知类型', trigger: 'change' }],
|
||||
content: [{ required: true, message: '请输入通知内容', trigger: 'blur' }],
|
||||
targetType: [{ required: true, message: '请选择目标用户类型', trigger: 'change' }],
|
||||
sendMethods: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: '请至少选择一种发送方式',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const mockData = ref<Notice[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'SIM卡更换通知-2026年1月批次',
|
||||
type: 'replace',
|
||||
content: '尊敬的用户,您的物联网卡将于近期进行更换,新卡将在3个工作日内寄出,请注意查收。',
|
||||
targetCount: 1000,
|
||||
sentCount: 980,
|
||||
failCount: 20,
|
||||
progress: 100,
|
||||
status: 'sent',
|
||||
sendMethods: ['sms', 'app'],
|
||||
createTime: '2026-01-09 09:00:00',
|
||||
sendTime: '2026-01-09 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '卡片激活成功通知',
|
||||
type: 'activate',
|
||||
content: '恭喜!您的物联网卡已成功激活,现在可以正常使用了。',
|
||||
targetCount: 500,
|
||||
sentCount: 350,
|
||||
failCount: 5,
|
||||
progress: 71,
|
||||
status: 'sending',
|
||||
sendMethods: ['sms', 'email'],
|
||||
createTime: '2026-01-09 11:30:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '套餐变更提醒',
|
||||
type: 'plan_change',
|
||||
content: '您好,您的套餐将于2026年2月1日起变更为新套餐,详情请登录系统查看。',
|
||||
targetCount: 800,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: ['sms'],
|
||||
createTime: '2026-01-09 14:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<Notice>({
|
||||
id: '',
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetCount: 0,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: [],
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) => item.title.includes(searchQuery.value) || item.content.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
data = data.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
if (typeFilter.value) {
|
||||
data = data.filter((item) => item.type === typeFilter.value)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
replace: '卡片更换',
|
||||
activate: '卡片激活',
|
||||
deactivate: '卡片停用',
|
||||
plan_change: '套餐变更'
|
||||
}
|
||||
return map[type] || '未知'
|
||||
}
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const map: Record<string, any> = {
|
||||
replace: 'warning',
|
||||
activate: 'success',
|
||||
deactivate: 'danger',
|
||||
plan_change: 'primary'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getSendMethodText = (method: string) => {
|
||||
const map: Record<string, string> = {
|
||||
sms: '短信',
|
||||
email: '邮件',
|
||||
app: 'App'
|
||||
}
|
||||
return map[method] || method
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Notice) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
title: row.title,
|
||||
type: row.type,
|
||||
content: row.content,
|
||||
targetType: 'all',
|
||||
sendMethods: row.sendMethods,
|
||||
scheduleSend: false,
|
||||
scheduleTime: null
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
title: '',
|
||||
type: 'replace',
|
||||
content: '',
|
||||
targetType: 'all',
|
||||
targetUsers: [],
|
||||
sendMethods: ['sms'],
|
||||
scheduleSend: false,
|
||||
scheduleTime: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.unshift({
|
||||
id: Date.now().toString(),
|
||||
title: form.title,
|
||||
type: form.type as any,
|
||||
content: form.content,
|
||||
targetCount: form.targetType === 'all' ? 1000 : form.targetUsers.length,
|
||||
sentCount: 0,
|
||||
failCount: 0,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
sendMethods: form.sendMethods,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('通知创建成功')
|
||||
} else {
|
||||
ElMessage.success('通知更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = (row: Notice) => {
|
||||
ElMessageBox.confirm(`确定立即发送通知"${row.title}"吗?`, '发送确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = 'sending'
|
||||
ElMessage.success('通知发送中...')
|
||||
|
||||
// 模拟发送进度
|
||||
const timer = setInterval(() => {
|
||||
if (row.progress < 100) {
|
||||
row.progress += 10
|
||||
row.sentCount = Math.floor((row.targetCount * row.progress) / 100)
|
||||
} else {
|
||||
clearInterval(timer)
|
||||
row.status = 'sent'
|
||||
row.sendTime = new Date().toLocaleString('zh-CN')
|
||||
ElMessage.success('通知发送完成')
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Notice) => {
|
||||
ElMessageBox.confirm('确定删除该通知吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) mockData.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewDetail = (row: Notice) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = () => {
|
||||
ElMessage.warning('最多只能上传1个文件')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.is-loading) {
|
||||
margin-right: 4px;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,694 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 导入操作区 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">批量导入设备</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载 CSV 模板文件,按照模板格式填写设备信息</p>
|
||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||
<p
|
||||
>4.
|
||||
<strong>必填字段:virtual_no(设备虚拟号)</strong
|
||||
>、device_name(设备名称)、device_model(设备型号)</p
|
||||
>
|
||||
<p
|
||||
>5.
|
||||
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||
>
|
||||
<p
|
||||
>6. virtual_no
|
||||
为空将导入失败,设备号重复将自动跳过,导入后可在任务管理中查看详情</p
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate"
|
||||
>下载导入模板</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<div class="upload-area">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 CSV 文件,且不超过 10MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<div style="margin-top: 16px; text-align: center">
|
||||
<ElButton
|
||||
type="success"
|
||||
:loading="uploading"
|
||||
:disabled="!fileList.length"
|
||||
@click="submitUpload"
|
||||
>
|
||||
开始导入
|
||||
</ElButton>
|
||||
<ElButton @click="clearFiles">清空</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-weight: 500">导入记录</span>
|
||||
<div>
|
||||
<ElSelect
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
style="width: 120px; margin-right: 12px"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="全部" :value="null" />
|
||||
<ElOption label="待处理" :value="1" />
|
||||
<ElOption label="处理中" :value="2" />
|
||||
<ElOption label="已完成" :value="3" />
|
||||
<ElOption label="失败" :value="4" />
|
||||
</ElSelect>
|
||||
<ElButton @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredRecords"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||
currentDetail.taskNo
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">{{ currentDetail.statusText }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||
currentDetail.fileName
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="警告数">
|
||||
<span style="color: var(--el-color-warning)">{{ currentDetail.warningCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始时间">{{ currentDetail.startedAt }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="完成时间">{{ currentDetail.completedAt }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createdAt }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||
currentDetail.errorMessage
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDivider content-position="left">失败明细</ElDivider>
|
||||
<div
|
||||
v-if="currentDetail.failReasons && currentDetail.failReasons.length"
|
||||
style="max-height: 300px; overflow-y: auto"
|
||||
>
|
||||
<ElTable :data="currentDetail.failReasons" border size="small">
|
||||
<ElTableColumn label="行号" prop="row" width="80" />
|
||||
<ElTableColumn label="设备编号" prop="deviceCode" width="150" />
|
||||
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||
<ElTableColumn label="失败原因" prop="message" min-width="200" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<ElEmpty v-else description="无失败记录" />
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
<ElButton
|
||||
v-if="currentDetail.failCount > 0"
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(currentDetail)"
|
||||
>
|
||||
下载失败数据
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, watch } from 'vue'
|
||||
import { ElMessage, ElTag, ElProgress, ElIcon, ElButton } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules/storage'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
|
||||
defineOptions({ name: 'DeviceImport' })
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface FailReason {
|
||||
row: number
|
||||
deviceCode: string
|
||||
iccid: string
|
||||
message: string
|
||||
}
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
taskNo: string
|
||||
status: number
|
||||
statusText: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
skipCount: number
|
||||
failCount: number
|
||||
warningCount: number
|
||||
startedAt: string
|
||||
completedAt: string
|
||||
errorMessage: string
|
||||
createdAt: string
|
||||
failReasons?: FailReason[]
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const tableRef = ref()
|
||||
const fileList = ref<File[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusFilter = ref<number | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
taskNo: '',
|
||||
status: 1,
|
||||
statusText: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
skipCount: 0,
|
||||
failCount: 0,
|
||||
warningCount: 0,
|
||||
startedAt: '',
|
||||
completedAt: '',
|
||||
errorMessage: '',
|
||||
createdAt: ''
|
||||
})
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
if (statusFilter.value === null) return importRecords.value
|
||||
return importRecords.value.filter((item) => item.status === statusFilter.value)
|
||||
})
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'taskNo',
|
||||
label: '任务编号',
|
||||
width: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 120,
|
||||
formatter: (row: ImportRecord) => {
|
||||
if (row.status === 1) {
|
||||
return h(ElTag, { type: 'info' }, () => '待处理')
|
||||
} else if (row.status === 2) {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: 'warning' },
|
||||
{
|
||||
default: () => [
|
||||
h(ElIcon, { class: 'is-loading' }, () => h(Loading)),
|
||||
h('span', { style: { marginLeft: '4px' } }, '处理中')
|
||||
]
|
||||
}
|
||||
)
|
||||
} else if (row.status === 3) {
|
||||
return h(ElTag, { type: 'success' }, () => '已完成')
|
||||
} else if (row.status === 4) {
|
||||
return h(ElTag, { type: 'danger' }, () => '失败')
|
||||
} else {
|
||||
return h(ElTag, { type: 'info' }, () => row.statusText || '-')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'batchNo',
|
||||
label: '批次号',
|
||||
width: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'fileName',
|
||||
label: '文件名',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'totalCount',
|
||||
label: '总数',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功数',
|
||||
width: 100,
|
||||
formatter: (row: ImportRecord) => {
|
||||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.successCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'skipCount',
|
||||
label: '跳过数',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '失败数',
|
||||
width: 100,
|
||||
formatter: (row: ImportRecord) => {
|
||||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.failCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'warningCount',
|
||||
label: '警告数',
|
||||
width: 100,
|
||||
formatter: (row: ImportRecord) => {
|
||||
return h('span', { style: { color: 'var(--el-color-warning)' } }, row.warningCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'startedAt',
|
||||
label: '开始时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'completedAt',
|
||||
label: '完成时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'errorMessage',
|
||||
label: '错误信息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'createdAt',
|
||||
label: '创建时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
formatter: (row: ImportRecord) => {
|
||||
const buttons = []
|
||||
|
||||
// 显示"查看详情"按钮
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '详情',
|
||||
onClick: () => viewDetail(row)
|
||||
})
|
||||
),
|
||||
buttons.push(
|
||||
h(ArtButtonTable, {
|
||||
text: '失败数据',
|
||||
type: 'danger',
|
||||
onClick: () => downloadFailData(row)
|
||||
})
|
||||
)
|
||||
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const downloadTemplate = () => {
|
||||
// CSV模板内容 - 包含表头和示例数据
|
||||
const csvContent = [
|
||||
// 表头
|
||||
'virtual_no,device_name,device_model,device_type,manufacturer,max_sim_slots',
|
||||
// 示例数据
|
||||
'DEV001,智能水表01,WM-2000,智能水表,华为,1',
|
||||
'DEV002,GPS定位器01,GPS-3000,定位设备,小米,2',
|
||||
'DEV003,智能燃气表01,GM-1500,智能燃气表,海尔,1'
|
||||
].join('\n')
|
||||
|
||||
// 添加 BOM 头确保 Excel 正确识别 UTF-8 编码
|
||||
const BOM = '\uFEFF'
|
||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', '设备导入模板.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
}
|
||||
|
||||
const handleFileChange = (uploadFile: any) => {
|
||||
// 验证文件大小(10MB限制)
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
||||
ElMessage.error('文件大小不能超过 10MB')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if (uploadFile.raw && !uploadFile.raw.name.endsWith('.csv')) {
|
||||
ElMessage.error('只能上传 CSV 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 三步上传流程
|
||||
* 1. 调用 StorageService.getUploadUrl() 获取预签名 URL 和 file_key
|
||||
* 2. 调用 StorageService.uploadFile() 上传文件到对象存储
|
||||
* 3. 调用 DeviceService.importDevices() 触发后端导入任务
|
||||
*/
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择CSV文件')
|
||||
return
|
||||
}
|
||||
|
||||
const file = fileList.value[0]
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
// 第一步:获取预签名上传 URL
|
||||
ElMessage.info('正在准备上传...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'text/csv',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
// 第二步:上传文件到对象存储
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(upload_url, file, 'text/csv')
|
||||
|
||||
// 第三步:调用设备导入API
|
||||
ElMessage.info('正在创建导入任务...')
|
||||
const importRes = await DeviceService.importDevices({
|
||||
file_key,
|
||||
batch_no: `DEV-${Date.now()}`
|
||||
})
|
||||
|
||||
if (importRes.code !== 0) {
|
||||
throw new Error(importRes.msg || '创建导入任务失败')
|
||||
}
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
|
||||
// 清空文件列表
|
||||
clearFiles()
|
||||
|
||||
// 刷新任务列表
|
||||
await fetchImportTasks()
|
||||
|
||||
// 显示成功消息
|
||||
ElMessage.success({
|
||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||
duration: 3000,
|
||||
showClose: true
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('设备导入失败:', error)
|
||||
ElMessage.error(error.message || '设备导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导入任务列表
|
||||
const fetchImportTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 100
|
||||
}
|
||||
|
||||
// 如果有状态筛选,添加到参数
|
||||
if (statusFilter.value !== null) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
|
||||
const res = await DeviceService.getImportTasks(params)
|
||||
if (res.code === 0 && res.data) {
|
||||
// 将API返回的数据映射到本地格式
|
||||
importRecords.value = res.data.items.map((item: any) => ({
|
||||
id: item.id.toString(),
|
||||
taskNo: item.task_no || '-',
|
||||
status: item.status,
|
||||
statusText: item.status_text || '-',
|
||||
batchNo: item.batch_no || '-',
|
||||
fileName: item.file_name || '-',
|
||||
totalCount: item.total_count || 0,
|
||||
successCount: item.success_count || 0,
|
||||
skipCount: item.skip_count || 0,
|
||||
failCount: item.fail_count || 0,
|
||||
warningCount: item.warning_count || 0,
|
||||
startedAt: item.started_at ? formatDateTime(item.started_at) : '-',
|
||||
completedAt: item.completed_at ? formatDateTime(item.completed_at) : '-',
|
||||
errorMessage: item.error_message || '-',
|
||||
createdAt: item.created_at ? formatDateTime(item.created_at) : '-'
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取导入任务列表失败:', error)
|
||||
ElMessage.error('获取导入任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
fetchImportTasks()
|
||||
}
|
||||
|
||||
const viewDetail = async (row: ImportRecord) => {
|
||||
try {
|
||||
const res = await DeviceService.getImportTaskDetail(Number(row.id))
|
||||
if (res.code === 0 && res.data) {
|
||||
const detail = res.data
|
||||
currentDetail.value = {
|
||||
id: detail.id.toString(),
|
||||
taskNo: detail.task_no || '-',
|
||||
status: detail.status,
|
||||
statusText: detail.status_text || '-',
|
||||
batchNo: detail.batch_no || '-',
|
||||
fileName: detail.file_name || '-',
|
||||
totalCount: detail.total_count || 0,
|
||||
successCount: detail.success_count || 0,
|
||||
skipCount: detail.skip_count || 0,
|
||||
failCount: detail.fail_count || 0,
|
||||
warningCount: detail.warning_count || 0,
|
||||
startedAt: detail.started_at ? formatDateTime(detail.started_at) : '-',
|
||||
completedAt: detail.completed_at ? formatDateTime(detail.completed_at) : '-',
|
||||
errorMessage: detail.error_message || '-',
|
||||
createdAt: detail.created_at ? formatDateTime(detail.created_at) : '-',
|
||||
failReasons:
|
||||
detail.failed_items?.map((item: any, index: number) => ({
|
||||
row: index + 1,
|
||||
deviceCode: item.virtual_no || '-',
|
||||
iccid: item.iccid || '-',
|
||||
message: item.reason || item.error || '未知错误'
|
||||
})) || []
|
||||
}
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务详情失败:', error)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
if (!row.failReasons || row.failReasons.length === 0) {
|
||||
ElMessage.warning('没有失败数据可下载')
|
||||
return
|
||||
}
|
||||
|
||||
// 生成失败数据CSV
|
||||
const headers = ['行号', '设备编号', 'ICCID', '失败原因']
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...row.failReasons.map((item) =>
|
||||
[item.row, item.deviceCode, item.iccid, `"${item.message}"`].join(',')
|
||||
)
|
||||
]
|
||||
const csvContent = csvRows.join('\n')
|
||||
|
||||
// 添加 BOM 头确保 Excel 正确识别 UTF-8 编码
|
||||
const BOM = '\uFEFF'
|
||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `导入失败数据_${row.batchNo}.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('失败数据下载成功')
|
||||
}
|
||||
|
||||
// 页面加载时获取任务列表
|
||||
onMounted(() => {
|
||||
fetchImportTasks()
|
||||
})
|
||||
|
||||
// 监听状态筛选变化
|
||||
watch(statusFilter, () => {
|
||||
fetchImportTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
margin-bottom: 16px;
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,386 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 导入操作区 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">批量导入网卡</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先下载模板文件,按照模板格式填写网卡信息</p>
|
||||
<p>2. 支持 Excel 格式(.xlsx, .xls),单次最多导入 1000 条</p>
|
||||
<p
|
||||
>3.
|
||||
<strong
|
||||
>必填字段:ICCID、virtual_no(虚拟号)、运营商、套餐类型、流量规格</strong
|
||||
></p
|
||||
>
|
||||
<p>4. <strong>Excel 文件必须包含 virtual_no 列,否则整批导入将失败</strong></p>
|
||||
<p>5. virtual_no 为空将导入失败,重复 ICCID 将跳过</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElButton type="primary" :icon="Download" @click="downloadTemplate"
|
||||
>下载导入模板</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :lg="12">
|
||||
<div class="upload-area">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:on-change="handleFileChange"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">只能上传 xlsx/xls 文件,且不超过 5MB</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
<div style="margin-top: 16px; text-align: center">
|
||||
<ElButton
|
||||
type="success"
|
||||
:loading="uploading"
|
||||
:disabled="!fileList.length"
|
||||
@click="submitUpload"
|
||||
>
|
||||
开始导入
|
||||
</ElButton>
|
||||
<ElButton @click="clearFiles">清空</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入记录 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-weight: 500">导入记录</span>
|
||||
<ElButton size="small" @click="refreshList">刷新</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="importRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
||||
<ElTableColumn label="总条数" prop="totalCount" width="100" />
|
||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">{{ scope.row.successCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.status === 'processing'" type="warning">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
处理中
|
||||
</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'success'" type="success">完成</ElTag>
|
||||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">失败</ElTag>
|
||||
<ElTag v-else type="info">待处理</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入进度" prop="progress" width="150">
|
||||
<template #default="scope">
|
||||
<ElProgress
|
||||
:percentage="scope.row.progress"
|
||||
:status="scope.row.status === 'failed' ? 'exception' : undefined"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="导入时间" prop="importTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="120" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.failCount > 0"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Download"
|
||||
@click="downloadFailData(scope.row)"
|
||||
>
|
||||
下载失败数据
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 导入详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="导入详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总条数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">
|
||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">
|
||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount || 0 }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败原因" :span="2">
|
||||
<div v-if="currentDetail.failReasons && currentDetail.failReasons.length">
|
||||
<div
|
||||
v-for="(reason, index) in currentDetail.failReasons"
|
||||
:key="index"
|
||||
style="margin-bottom: 4px"
|
||||
>
|
||||
<ElTag type="danger" size="small">行{{ reason.row }}</ElTag>
|
||||
{{ reason.message }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="color: var(--el-text-color-secondary)">无</span>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance, UploadRawFile } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimImport' })
|
||||
|
||||
interface ImportRecord {
|
||||
id: string
|
||||
batchNo: string
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
skipCount?: number
|
||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
||||
progress: number
|
||||
importTime: string
|
||||
operator: string
|
||||
failReasons?: Array<{ row: number; message: string }>
|
||||
}
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadUrl = ref('/api/batch/sim-import')
|
||||
const fileList = ref<UploadRawFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
const importRecords = ref<ImportRecord[]>([
|
||||
{
|
||||
id: '1',
|
||||
batchNo: 'IMP20260109001',
|
||||
fileName: '网卡导入模板_20260109.xlsx',
|
||||
totalCount: 500,
|
||||
successCount: 495,
|
||||
failCount: 5,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-09 10:30:00',
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 23, message: 'ICCID 格式错误' },
|
||||
{ row: 45, message: 'ICCID 已存在' },
|
||||
{ row: 67, message: '套餐类型不存在' },
|
||||
{ row: 89, message: '流量规格格式错误' },
|
||||
{ row: 123, message: '运营商代码无效' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
batchNo: 'IMP20260108001',
|
||||
fileName: '网卡批量导入.xlsx',
|
||||
totalCount: 1000,
|
||||
successCount: 1000,
|
||||
failCount: 0,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-08 15:20:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
batchNo: 'IMP20260107001',
|
||||
fileName: '测试数据.xlsx',
|
||||
totalCount: 200,
|
||||
successCount: 150,
|
||||
failCount: 50,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: '2026-01-07 09:15:00',
|
||||
operator: 'operator01',
|
||||
failReasons: [
|
||||
{ row: 10, message: 'ICCID 重复' },
|
||||
{ row: 20, message: '运营商字段为空' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const currentDetail = ref<ImportRecord>({
|
||||
id: '',
|
||||
batchNo: '',
|
||||
fileName: '',
|
||||
totalCount: 0,
|
||||
successCount: 0,
|
||||
failCount: 0,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
importTime: '',
|
||||
operator: ''
|
||||
})
|
||||
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('模板下载中...')
|
||||
// 实际项目中应该调用下载接口
|
||||
setTimeout(() => {
|
||||
ElMessage.success('模板下载成功')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleFileChange = (file: any, files: any[]) => {
|
||||
fileList.value = files
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
ElMessage.info('正在导入,请稍候...')
|
||||
|
||||
// 模拟上传和导入过程
|
||||
setTimeout(() => {
|
||||
const newRecord: ImportRecord = {
|
||||
id: Date.now().toString(),
|
||||
batchNo: `IMP${new Date().getTime()}`,
|
||||
fileName: fileList.value[0].name,
|
||||
totalCount: 300,
|
||||
successCount: 295,
|
||||
failCount: 5,
|
||||
skipCount: 0,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
importTime: new Date().toLocaleString('zh-CN'),
|
||||
operator: 'admin',
|
||||
failReasons: [
|
||||
{ row: 12, message: 'ICCID 格式错误' },
|
||||
{ row: 34, message: 'ICCID 已存在' }
|
||||
]
|
||||
}
|
||||
|
||||
importRecords.value.unshift(newRecord)
|
||||
uploading.value = false
|
||||
clearFiles()
|
||||
ElMessage.success(
|
||||
`导入完成!成功 ${newRecord.successCount} 条,失败 ${newRecord.failCount} 条`
|
||||
)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const viewDetail = (row: ImportRecord) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const downloadFailData = (row: ImportRecord) => {
|
||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
||||
setTimeout(() => {
|
||||
ElMessage.success('下载完成')
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon--upload) {
|
||||
margin-bottom: 16px;
|
||||
font-size: 67px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
:deep(.el-upload__text) {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-loading) {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,383 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-template-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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>
|
||||
<ElButton @click="batchOperation" :disabled="selectedRows.length === 0"
|
||||
>批量操作</ElButton
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="400px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="formData.name" placeholder="请输入名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description">
|
||||
<ElInput
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessageBox, ElMessage, FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardTemplate' })
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
status: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
name: '示例项目1',
|
||||
description: '这是一个示例项目',
|
||||
status: '1',
|
||||
statusName: '正常',
|
||||
createTime: '2024-11-07 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '示例项目2',
|
||||
description: '这是另一个示例项目',
|
||||
status: '2',
|
||||
statusName: '禁用',
|
||||
createTime: '2024-11-06 15:30:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '名称',
|
||||
prop: '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' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ID', prop: 'id' },
|
||||
{ label: '名称', prop: 'name' },
|
||||
{ label: '描述', prop: 'description' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '创建时间', prop: 'createTime' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return '正常'
|
||||
case '2':
|
||||
return '禁用'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.name = row.name
|
||||
formData.description = row.description
|
||||
} else {
|
||||
formData.name = ''
|
||||
formData.description = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const deleteItem = () => {
|
||||
ElMessageBox.confirm('确定要删除该项目吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getDataList()
|
||||
})
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
const batchOperation = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要操作的项目')
|
||||
return
|
||||
}
|
||||
ElMessage.info(`已选择 ${selectedRows.value.length} 个项目`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'name',
|
||||
label: '名称',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: '描述',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTagType(row.status) }, () => buildTagText(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteItem()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
// 获取数据列表
|
||||
const getDataList = 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
|
||||
} catch (error) {
|
||||
console.error('获取数据列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
getDataList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-template-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
</style>
|
||||
@@ -1,436 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-assign-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showDistributeDialog">分销网卡</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="分销网卡"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="选择代理商" prop="agent">
|
||||
<ElSelect
|
||||
v-model="formData.agent"
|
||||
placeholder="请选择代理商"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="agent in agentOptions"
|
||||
:key="agent.value"
|
||||
:label="agent.label"
|
||||
:value="agent.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认分销
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
ElSelect,
|
||||
ElOption
|
||||
} from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
|
||||
defineOptions({ name: 'CardAssign' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
excelFile: null as File | null,
|
||||
agent: ''
|
||||
})
|
||||
|
||||
// 代理商选项
|
||||
const agentOptions = ref([
|
||||
{ label: '张丽丽', value: 'zhangll' },
|
||||
{ label: '王小明', value: 'wangxm' },
|
||||
{ label: 'HNSXKJ', value: 'hnsxkj' },
|
||||
{ label: '孔丽娟', value: 'konglj' },
|
||||
{ label: '李佳音', value: 'lijy' },
|
||||
{ label: '赵强', value: 'zhaoq' }
|
||||
])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
importTime: '2025-11-07 15:01:37',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
importCount: 1000,
|
||||
successCount: 998,
|
||||
failCount: 2,
|
||||
distributor: '张丽丽',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
importTime: '2025-11-05 12:07:41',
|
||||
packageName: '如意包年3G流量包',
|
||||
importCount: 500,
|
||||
successCount: 500,
|
||||
failCount: 0,
|
||||
distributor: '王小明',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
importTime: '2025-11-03 11:21:42',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
importCount: 200,
|
||||
successCount: 195,
|
||||
failCount: 5,
|
||||
distributor: 'HNSXKJ',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
importTime: '2025-10-29 16:01:16',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
importCount: 2500,
|
||||
successCount: 2500,
|
||||
failCount: 0,
|
||||
distributor: '未分销',
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
importTime: '2025-10-22 15:44:31',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
importCount: 800,
|
||||
successCount: 795,
|
||||
failCount: 5,
|
||||
distributor: '孔丽娟',
|
||||
operator: '孔丽娟'
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '导入时间', prop: 'importTime' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '导入张数(张)', prop: 'importCount' },
|
||||
{ label: '导入成功张数(张)', prop: 'successCount' },
|
||||
{ label: '导入失败张数(张)', prop: 'failCount' },
|
||||
{ label: '分销商', prop: 'distributor' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.packageName} 的分配详情`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该分配记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCardAssignList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'importTime',
|
||||
label: '导入时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'importCount',
|
||||
label: '导入张数(张)',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '导入成功张数(张)',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '导入失败张数(张)',
|
||||
width: 150,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.failCount > 0 ? 'danger' : 'success' }, () => row.failCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'distributor',
|
||||
label: '分销商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardAssignList()
|
||||
})
|
||||
|
||||
// 获取网卡分配列表
|
||||
const getCardAssignList = 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
|
||||
} catch (error) {
|
||||
console.error('获取网卡分配列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardAssignList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardAssignList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardAssignList()
|
||||
}
|
||||
|
||||
// 显示分销对话框
|
||||
const showDistributeDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.excelFile = null
|
||||
formData.agent = ''
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
formData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }],
|
||||
agent: [{ required: true, message: '请选择代理商', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!formData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`已成功分销给代理商:${agentOptions.value.find((a) => a.value === formData.agent)?.label}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getCardAssignList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-assign-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,549 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-change-card-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showAddDialog">新增</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="新增换卡网卡"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="ICCID" prop="iccid">
|
||||
<ElInput v-model="formData.iccid" placeholder="请输入ICCID" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否收费" prop="isCharged">
|
||||
<ElSelect
|
||||
v-model="formData.isCharged"
|
||||
placeholder="请选择是否收费"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
@change="handleChargeChange"
|
||||
>
|
||||
<ElOption label="收费" value="true" />
|
||||
<ElOption label="免费" value="false" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="收费金额" prop="chargeAmount" v-if="formData.isCharged === 'true'">
|
||||
<ElInputNumber
|
||||
v-model="formData.chargeAmount"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入收费金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElSelect, ElOption, ElInputNumber } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardChangeCard' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
cardNumber: '',
|
||||
accessNumber: '',
|
||||
cardCompany: '',
|
||||
operationDateRange: '',
|
||||
isCharged: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const formData = reactive({
|
||||
iccid: '',
|
||||
isCharged: '',
|
||||
chargeAmount: 0
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
cardNumber: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
cardCompany: '联通2',
|
||||
agentLevel: '一级代理',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00',
|
||||
isCharged: '收费',
|
||||
amount: '50.00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cardNumber: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
cardCompany: 'SXKJ-NB',
|
||||
agentLevel: '二级代理',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00',
|
||||
isCharged: '免费',
|
||||
amount: '0.00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cardNumber: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
cardCompany: '联通36',
|
||||
agentLevel: '终端用户',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00',
|
||||
isCharged: '收费',
|
||||
amount: '30.00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cardNumber: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
cardCompany: '联通1-1',
|
||||
agentLevel: '一级代理',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00',
|
||||
isCharged: '收费',
|
||||
amount: '100.00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cardNumber: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
cardCompany: '广电4',
|
||||
agentLevel: '分销商',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00',
|
||||
isCharged: '免费',
|
||||
amount: '0.00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '卡号',
|
||||
prop: 'cardNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入卡号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '接入号',
|
||||
prop: 'accessNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入接入号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '操作时间',
|
||||
prop: 'operationDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '是否收费',
|
||||
prop: 'isCharged',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '收费', value: 'charged' },
|
||||
{ label: '免费', value: 'free' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '卡号', prop: 'cardNumber' },
|
||||
{ label: '接入号', prop: 'accessNumber' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '代理商层级', prop: 'agentLevel' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '是否收费', prop: 'isCharged' },
|
||||
{ label: '金额', prop: 'amount' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取收费状态标签类型
|
||||
const getChargeStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '收费':
|
||||
return 'warning'
|
||||
case '免费':
|
||||
return 'success'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条换卡网卡记录`)
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showAddDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.iccid = ''
|
||||
formData.isCharged = ''
|
||||
formData.chargeAmount = 0
|
||||
}
|
||||
|
||||
// 收费方式变化处理
|
||||
const handleChargeChange = (value: string) => {
|
||||
if (value === 'false') {
|
||||
formData.chargeAmount = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看换卡网卡详情: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑换卡网卡记录: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该换卡网卡记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCardChangeCardList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'cardNumber',
|
||||
label: '卡号',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号'
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司'
|
||||
},
|
||||
{
|
||||
prop: 'agentLevel',
|
||||
label: '代理商层级'
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人'
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'isCharged',
|
||||
label: '是否收费',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getChargeStatusType(row.isCharged) }, () => row.isCharged)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'amount',
|
||||
label: '金额',
|
||||
formatter: (row) => `¥${row.amount}`
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardChangeCardList()
|
||||
})
|
||||
|
||||
// 获取换卡网卡列表
|
||||
const getCardChangeCardList = 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
|
||||
} catch (error) {
|
||||
console.error('获取换卡网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardChangeCardList()
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
iccid: [
|
||||
{ required: true, message: '请输入ICCID', trigger: 'blur' },
|
||||
{ min: 15, max: 20, message: 'ICCID长度在 15 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
isCharged: [{ required: true, message: '请选择是否收费', trigger: 'change' }],
|
||||
chargeAmount: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入收费金额',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (formData.isCharged === 'true' && (!value || value <= 0)) {
|
||||
callback(new Error('收费时金额必须大于0'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
const chargeText = formData.isCharged === 'true' ? '收费' : '免费'
|
||||
const amountText =
|
||||
formData.isCharged === 'true' ? `,金额:¥${formData.chargeAmount}` : ''
|
||||
ElMessage.success(
|
||||
`新增换卡网卡成功!ICCID:${formData.iccid},${chargeText}${amountText}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getCardChangeCardList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-change-card-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,607 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-list-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 数据视图组件 -->
|
||||
<ArtDataViewer
|
||||
ref="dataViewerRef"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:table-columns="columns"
|
||||
:descriptions-fields="descriptionsFields"
|
||||
:descriptions-columns="2"
|
||||
:pagination="pagination"
|
||||
:card-title-field="'importBatch'"
|
||||
:label-width="'150px'"
|
||||
:field-columns="columnChecks"
|
||||
:show-card-actions="true"
|
||||
:show-card-selection="true"
|
||||
:default-view="currentView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@view-change="handleViewChange"
|
||||
>
|
||||
<template #header-left>
|
||||
<ElButton type="primary" @click="showDialog('add')">导入网卡</ElButton>
|
||||
<ElButton @click="showDialog('single')">单卡导入</ElButton>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:currentView="currentView"
|
||||
@refresh="handleRefresh"
|
||||
@viewChange="handleViewChange"
|
||||
:show-title="false"
|
||||
:show-view-toggle="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ item }">
|
||||
<ArtButtonTable type="view" text="查看失败" @click="viewFailures(item)" />
|
||||
</template>
|
||||
</ArtDataViewer>
|
||||
|
||||
<!-- 网卡信息对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '导入网卡' : dialogType === 'single' ? '单卡导入' : '编辑'"
|
||||
width="40%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<ElFormItem label="ICCID" prop="iccid">
|
||||
<ElInput v-model="formData.iccid" placeholder="请输入ICCID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="IMSI" prop="imsi">
|
||||
<ElInput v-model="formData.imsi" placeholder="请输入IMSI" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号码" prop="msisdn">
|
||||
<ElInput v-model="formData.msisdn" placeholder="请输入手机号码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="运营商" prop="operator">
|
||||
<ElSelect v-model="formData.operator" placeholder="请选择运营商">
|
||||
<ElOption label="中国移动" value="mobile" />
|
||||
<ElOption label="中国联通" value="unicom" />
|
||||
<ElOption label="中国电信" value="telecom" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="网络类型" prop="networkType">
|
||||
<ElSelect v-model="formData.networkType" placeholder="请选择网络类型">
|
||||
<ElOption label="2G" value="2G" />
|
||||
<ElOption label="3G" value="3G" />
|
||||
<ElOption label="4G" value="4G" />
|
||||
<ElOption label="5G" value="5G" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSelect v-model="formData.status" placeholder="请选择状态">
|
||||
<ElOption label="激活" value="1" />
|
||||
<ElOption label="停用" value="2" />
|
||||
<ElOption label="测试" value="3" />
|
||||
<ElOption label="库存" value="4" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElDialog, FormInstance, ElTag } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardList' })
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 当前视图模式
|
||||
const currentView = ref('table')
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
operator: '',
|
||||
distributor: '',
|
||||
importDateRange: '',
|
||||
cardCompany: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 数据视图组件引用
|
||||
const dataViewerRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
importTime: '2025-11-07 15:01:37.0',
|
||||
importBatch: '650',
|
||||
importCount: 1,
|
||||
successCount: 1,
|
||||
failCount: 0,
|
||||
distributorName: '未分销',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
cardCompanyName: '联通2',
|
||||
operatorName: '中国联通',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
importTime: '2025-11-05 12:07:41.0',
|
||||
importBatch: '649',
|
||||
importCount: 100,
|
||||
successCount: 100,
|
||||
failCount: 0,
|
||||
distributorName: '未分销',
|
||||
packageName: '如意包年3G流量包',
|
||||
cardCompanyName: '联通36',
|
||||
operatorName: '中国联通',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
importTime: '2025-11-03 11:21:42.0',
|
||||
importBatch: '648',
|
||||
importCount: 200,
|
||||
successCount: 200,
|
||||
failCount: 0,
|
||||
distributorName: 'HNSXKJ',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
cardCompanyName: 'SXKJ-NB',
|
||||
operatorName: 'GS移动',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
importTime: '2025-10-29 16:01:16.0',
|
||||
importBatch: '647',
|
||||
importCount: 2500,
|
||||
successCount: 2500,
|
||||
failCount: 0,
|
||||
distributorName: '未分销',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
cardCompanyName: '联通1-1',
|
||||
operatorName: '中国联通',
|
||||
cardType: '月卡',
|
||||
operatorUser: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
importTime: '2025-10-22 15:44:31.0',
|
||||
importBatch: '644',
|
||||
importCount: 500,
|
||||
successCount: 499,
|
||||
failCount: 1,
|
||||
distributorName: '未分销',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
cardCompanyName: '广电4',
|
||||
operatorName: 'GDWL',
|
||||
cardType: '月卡',
|
||||
operatorUser: '孔丽娟'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '运营商',
|
||||
prop: 'operator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '中国移动', value: 'mobile' },
|
||||
{ label: '中国联通', value: 'unicom' },
|
||||
{ label: '中国电信', value: 'telecom' },
|
||||
{ label: 'GS移动', value: 'gs_mobile' },
|
||||
{ label: 'DC物联', value: 'dc_iot' },
|
||||
{ label: 'GDWL', value: 'gdwl' },
|
||||
{ label: 'GS联通', value: 'gs_unicom' },
|
||||
{ label: 'GS电信', value: 'gs_telecom' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入分销商'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '导入时间',
|
||||
prop: 'importDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '新移动22', value: 'new_mobile22' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '联通38', value: 'unicom38' },
|
||||
{ label: '电信9', value: 'telecom9' },
|
||||
{ label: '联通10-1', value: 'unicom10_1' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '导入时间', prop: 'importTime' },
|
||||
{ label: '导入批次', prop: 'importBatch' },
|
||||
{ label: '导入张数(张)', prop: 'importCount' },
|
||||
{ label: '导入成功张数(张)', prop: 'successCount' },
|
||||
{ label: '导入失败张数(张)', prop: 'failCount' },
|
||||
{ label: '分销代理商', prop: 'distributorName' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '开卡公司', prop: 'cardCompanyName' },
|
||||
{ label: '运营商', prop: 'operatorName' },
|
||||
{ label: '卡类型', prop: 'cardType' },
|
||||
{ label: '操作人', prop: 'operatorUser' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 描述字段配置
|
||||
const descriptionsFields = [
|
||||
{ prop: 'importTime', label: '导入时间' },
|
||||
{ prop: 'importBatch', label: '导入批次' },
|
||||
{ prop: 'importCount', label: '导入张数(张)' },
|
||||
{ prop: 'successCount', label: '导入成功张数(张)' },
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '导入失败张数(张)',
|
||||
formatter: (row: any) => {
|
||||
const type = row.failCount > 0 ? 'danger' : 'success'
|
||||
return `<el-tag type="${type}" size="small">${row.failCount}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'distributorName', label: '分销代理商' },
|
||||
{ prop: 'packageName', label: '套餐名称', span: 1 },
|
||||
{ prop: 'cardCompanyName', label: '开卡公司' },
|
||||
{ prop: 'operatorName', label: '运营商' },
|
||||
{ prop: 'cardType', label: '卡类型' },
|
||||
{ prop: 'operatorUser', label: '操作人' }
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'danger'
|
||||
case '3':
|
||||
return 'warning'
|
||||
case '4':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return '激活'
|
||||
case '2':
|
||||
return '停用'
|
||||
case '3':
|
||||
return '测试'
|
||||
case '4':
|
||||
return '库存'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.iccid = row.iccid
|
||||
formData.imsi = row.imsi
|
||||
formData.msisdn = row.msisdn
|
||||
formData.operator = row.operator
|
||||
formData.networkType = row.networkType
|
||||
formData.status = row.status
|
||||
} else {
|
||||
formData.iccid = ''
|
||||
formData.imsi = ''
|
||||
formData.msisdn = ''
|
||||
formData.operator = ''
|
||||
formData.networkType = ''
|
||||
formData.status = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 查看失败
|
||||
const viewFailures = (row: any) => {
|
||||
if (row.failCount > 0) {
|
||||
ElMessage.info(`查看导入批次 ${row.importBatch} 的失败记录`)
|
||||
// 这里可以跳转到失败详情页面或显示失败详情对话框
|
||||
} else {
|
||||
ElMessage.info('该批次没有失败记录')
|
||||
}
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
const batchOperation = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要操作的导入记录')
|
||||
return
|
||||
}
|
||||
ElMessage.info(`已选择 ${selectedRows.value.length} 条导入记录`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'importTime',
|
||||
label: '导入时间',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
prop: 'importBatch',
|
||||
label: '导入批次',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'importCount',
|
||||
label: '导入张数(张)',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '导入成功张数(张)',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'failCount',
|
||||
label: '导入失败张数(张)',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '分销代理商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'cardCompanyName',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operatorName',
|
||||
label: '运营商',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cardType',
|
||||
label: '卡类型',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'operatorUser',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'view',
|
||||
text: '查看失败',
|
||||
onClick: () => viewFailures(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
iccid: '',
|
||||
imsi: '',
|
||||
msisdn: '',
|
||||
operator: '',
|
||||
networkType: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getCardList()
|
||||
})
|
||||
|
||||
// 获取网卡列表
|
||||
const getCardList = 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
|
||||
} catch (error) {
|
||||
console.error('获取网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
iccid: [
|
||||
{ required: true, message: '请输入ICCID', trigger: 'blur' },
|
||||
{ min: 19, max: 20, message: 'ICCID长度应为19-20位', trigger: 'blur' }
|
||||
],
|
||||
imsi: [
|
||||
{ required: true, message: '请输入IMSI', trigger: 'blur' },
|
||||
{ min: 15, max: 15, message: 'IMSI长度应为15位', trigger: 'blur' }
|
||||
],
|
||||
msisdn: [
|
||||
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
operator: [{ required: true, message: '请选择运营商', trigger: 'change' }],
|
||||
networkType: [{ required: true, message: '请选择网络类型', trigger: 'change' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
getCardList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardList()
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
const handleViewChange = (view: string) => {
|
||||
console.log('视图切换到:', view)
|
||||
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list-page {
|
||||
// Card list page styles
|
||||
}
|
||||
</style>
|
||||
@@ -1,695 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-replacement-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">搜索</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
<ElButton type="success" @click="showImportDialog">导入excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 导入Excel对话框 -->
|
||||
<ElDialog
|
||||
v-model="importDialogVisible"
|
||||
title="导入换卡记录"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm
|
||||
ref="importFormRef"
|
||||
:model="importFormData"
|
||||
:rules="importRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="importFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
|
||||
确认导入
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElUpload, ElIcon, ElDivider } from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardReplacement' })
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const importLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
processResult: '',
|
||||
cardCompany: '',
|
||||
oldSimNumber: '',
|
||||
newSimNumber: '',
|
||||
newCardOperator: '',
|
||||
submitDateRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 导入表单实例
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 导入表单数据
|
||||
const importFormData = reactive({
|
||||
excelFile: null as File | null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccid: '89860621370079892035',
|
||||
currentPackage: '随意联畅玩年卡套餐(12个月)',
|
||||
cardCompany: '联通2',
|
||||
hierarchyRelation: '一级代理->二级代理->终端用户',
|
||||
recipientName: '张丽丽',
|
||||
recipientPhone: '138****5678',
|
||||
recipientAddress: '北京市朝阳区建国门外大街1号',
|
||||
newSimNumber: '1440012345778',
|
||||
processor: '张若暄',
|
||||
processTime: '2025-11-08 14:30:00',
|
||||
processDescription: '用户申请换卡,原卡损坏',
|
||||
processStatus: '处理完成',
|
||||
submitTime: '2025-11-08 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccid: '89860621370079892036',
|
||||
currentPackage: '如意包年3G流量包',
|
||||
cardCompany: '联通36',
|
||||
hierarchyRelation: '总代理->分销商',
|
||||
recipientName: '王小明',
|
||||
recipientPhone: '139****1234',
|
||||
recipientAddress: '上海市浦东新区陆家嘴环路1000号',
|
||||
newSimNumber: '1440012345779',
|
||||
processor: '孔丽娟',
|
||||
processTime: '2025-11-07 16:15:00',
|
||||
processDescription: '卡槽损坏需要更换新卡',
|
||||
processStatus: '处理中',
|
||||
submitTime: '2025-11-07 09:30:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccid: '89860621370079892037',
|
||||
currentPackage: 'Y-NB专享套餐',
|
||||
cardCompany: 'SXKJ-NB',
|
||||
hierarchyRelation: '直营',
|
||||
recipientName: 'HNSXKJ',
|
||||
recipientPhone: '135****9876',
|
||||
recipientAddress: '广州市天河区珠江新城花城大道5号',
|
||||
newSimNumber: '1440012345780',
|
||||
processor: '李佳音',
|
||||
processTime: '2025-11-06 11:20:00',
|
||||
processDescription: '设备升级需要更换新规格SIM卡',
|
||||
processStatus: '审核中',
|
||||
submitTime: '2025-11-06 08:45:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccid: '89860621370079892038',
|
||||
currentPackage: '100G全国流量月卡套餐',
|
||||
cardCompany: '联通1-1',
|
||||
hierarchyRelation: '一级代理->终端用户',
|
||||
recipientName: '赵强',
|
||||
recipientPhone: '137****5555',
|
||||
recipientAddress: '深圳市南山区科技园南区深南大道10000号',
|
||||
newSimNumber: '1440012345781',
|
||||
processor: '张若暄',
|
||||
processTime: '',
|
||||
processDescription: '',
|
||||
processStatus: '待处理',
|
||||
submitTime: '2025-11-05 15:10:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccid: '89860621370079892039',
|
||||
currentPackage: '广电飞悦卡无预存50G(30天)',
|
||||
cardCompany: '广电4',
|
||||
hierarchyRelation: '二级代理->终端用户',
|
||||
recipientName: '李丽',
|
||||
recipientPhone: '133****7777',
|
||||
recipientAddress: '杭州市西湖区文三路90号',
|
||||
newSimNumber: '1440012345782',
|
||||
processor: '赵强',
|
||||
processTime: '2025-11-04 09:30:00',
|
||||
processDescription: '换卡申请已拒绝,原因:信息不完整',
|
||||
processStatus: '已拒绝',
|
||||
submitTime: '2025-11-04 08:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '处理结果',
|
||||
prop: 'processResult',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '处理完成', value: 'completed' },
|
||||
{ label: '处理中', value: 'processing' },
|
||||
{ label: '审核中', value: 'reviewing' },
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '已拒绝', value: 'rejected' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '换卡SIM号',
|
||||
prop: 'oldSimNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入原SIM号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '新SIM号',
|
||||
prop: 'newSimNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入新SIM号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '新卡运营商',
|
||||
prop: 'newCardOperator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '中国移动', value: 'mobile' },
|
||||
{ label: '中国联通', value: 'unicom' },
|
||||
{ label: '中国电信', value: 'telecom' },
|
||||
{ label: 'GS移动', value: 'gs_mobile' },
|
||||
{ label: 'DC物联', value: 'dc_iot' },
|
||||
{ label: 'GDWL', value: 'gdwl' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '提交时间',
|
||||
prop: 'submitDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID号', prop: 'iccid' },
|
||||
{ label: '当前套餐', prop: 'currentPackage' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '层级关系', prop: 'hierarchyRelation' },
|
||||
{ label: '收货人姓名', prop: 'recipientName' },
|
||||
{ label: '收货人电话', prop: 'recipientPhone' },
|
||||
{ label: '收货人地址', prop: 'recipientAddress' },
|
||||
{ label: '新sim号', prop: 'newSimNumber' },
|
||||
{ label: '处理人', prop: 'processor' },
|
||||
{ label: '处理时间', prop: 'processTime' },
|
||||
{ label: '处理描述', prop: 'processDescription' },
|
||||
{ label: '处理状态', prop: 'processStatus' },
|
||||
{ label: '提交时间', prop: 'submitTime' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取处理状态标签类型
|
||||
const getProcessStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '处理完成':
|
||||
return 'success'
|
||||
case '处理中':
|
||||
return 'warning'
|
||||
case '审核中':
|
||||
return 'info'
|
||||
case '待处理':
|
||||
return 'primary'
|
||||
case '已拒绝':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条换卡记录`)
|
||||
}
|
||||
|
||||
// 显示导入对话框
|
||||
const showImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (importFormRef.value) {
|
||||
importFormRef.value.resetFields()
|
||||
}
|
||||
importFormData.excelFile = null
|
||||
importFormData.remark = ''
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载换卡记录导入模板...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看换卡详情: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑换卡记录: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该换卡记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCardReplacementList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 处理换卡申请
|
||||
const processApplication = (row: any) => {
|
||||
if (row.processStatus === '处理完成' || row.processStatus === '已拒绝') {
|
||||
ElMessage.warning('该申请已处理完成')
|
||||
return
|
||||
}
|
||||
ElMessage.info(`处理换卡申请: ${row.iccid}`)
|
||||
// 这里可以打开处理对话框或跳转到处理页面
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'currentPackage',
|
||||
label: '当前套餐',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'hierarchyRelation',
|
||||
label: '层级关系',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'recipientName',
|
||||
label: '收货人姓名',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'recipientPhone',
|
||||
label: '收货人电话',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'recipientAddress',
|
||||
label: '收货人地址',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'newSimNumber',
|
||||
label: '新sim号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'processor',
|
||||
label: '处理人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'processTime',
|
||||
label: '处理时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'processDescription',
|
||||
label: '处理描述',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'processStatus',
|
||||
label: '处理状态',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getProcessStatusType(row.processStatus) }, () => row.processStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'submitTime',
|
||||
label: '提交时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 280,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '处理',
|
||||
disabled: row.processStatus === '处理完成' || row.processStatus === '已拒绝',
|
||||
onClick: () => processApplication(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardReplacementList()
|
||||
})
|
||||
|
||||
// 获取换卡管理列表
|
||||
const getCardReplacementList = 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
|
||||
} catch (error) {
|
||||
console.error('获取换卡管理列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardReplacementList()
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
importFormData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单验证规则
|
||||
const importRules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交导入
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importFormRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!importFormData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await importFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
importLoading.value = true
|
||||
|
||||
// 模拟导入过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success('换卡记录导入成功!')
|
||||
importDialogVisible.value = false
|
||||
importLoading.value = false
|
||||
getCardReplacementList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-replacement-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,460 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-shutdown-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showShutdownDialog">设置停机时间</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="设置停机时间"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="停机类型" prop="shutdownType">
|
||||
<ElSelect
|
||||
v-model="formData.shutdownType"
|
||||
placeholder="请选择停机类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="立即停机" value="immediate" />
|
||||
<ElOption label="定时停机" value="scheduled" />
|
||||
<ElOption label="批量停机" value="batch" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
label="停机时间"
|
||||
prop="shutdownTime"
|
||||
v-if="formData.shutdownType === 'scheduled'"
|
||||
>
|
||||
<ElDatePicker
|
||||
v-model="formData.shutdownTime"
|
||||
type="datetime"
|
||||
placeholder="请选择停机时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="停机原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="formData.reason"
|
||||
type="textarea"
|
||||
placeholder="请输入停机原因"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="影响范围" prop="affectedCards">
|
||||
<ElInputNumber
|
||||
v-model="formData.affectedCards"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
placeholder="请输入影响的网卡数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="通知方式" prop="notifyMethod">
|
||||
<ElCheckboxGroup v-model="formData.notifyMethod">
|
||||
<ElCheckbox label="短信通知">短信通知</ElCheckbox>
|
||||
<ElCheckbox label="邮件通知">邮件通知</ElCheckbox>
|
||||
<ElCheckbox label="系统通知">系统通知</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认设置
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElDatePicker,
|
||||
ElInputNumber,
|
||||
ElCheckboxGroup,
|
||||
ElCheckbox
|
||||
} from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
|
||||
defineOptions({ name: 'CardShutdown' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
shutdownType: '',
|
||||
shutdownTime: '',
|
||||
reason: '',
|
||||
affectedCards: 1,
|
||||
notifyMethod: [] as string[]
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
totalCount: 1000,
|
||||
operationTime: '2025-11-08 14:30:00',
|
||||
successCount: 998,
|
||||
failureCount: 2,
|
||||
operationType: '立即停机',
|
||||
operator: '张若暄',
|
||||
reason: '系统维护'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
totalCount: 500,
|
||||
operationTime: '2025-11-07 10:15:00',
|
||||
successCount: 500,
|
||||
failureCount: 0,
|
||||
operationType: '定时停机',
|
||||
operator: '孔丽娟',
|
||||
reason: '网络升级'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
totalCount: 200,
|
||||
operationTime: '2025-11-06 16:45:00',
|
||||
successCount: 195,
|
||||
failureCount: 5,
|
||||
operationType: '批量停机',
|
||||
operator: '张若暄',
|
||||
reason: '安全检查'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
totalCount: 1500,
|
||||
operationTime: '2025-11-05 09:30:00',
|
||||
successCount: 1500,
|
||||
failureCount: 0,
|
||||
operationType: '立即停机',
|
||||
operator: '李佳音',
|
||||
reason: '应急处理'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
totalCount: 800,
|
||||
operationTime: '2025-11-04 13:20:00',
|
||||
successCount: 792,
|
||||
failureCount: 8,
|
||||
operationType: '定时停机',
|
||||
operator: '张若暄',
|
||||
reason: '定期维护'
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '总数', prop: 'totalCount' },
|
||||
{ label: '时间', prop: 'operationTime' },
|
||||
{ label: '成功', prop: 'successCount' },
|
||||
{ label: '失败', prop: 'failureCount' },
|
||||
{ label: '操作类型', prop: 'operationType' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取操作类型标签类型
|
||||
const getOperationTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case '立即停机':
|
||||
return 'danger'
|
||||
case '定时停机':
|
||||
return 'warning'
|
||||
case '批量停机':
|
||||
return 'info'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看停机操作的详细信息: ${row.operationType}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该停机记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getShutdownList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 重新执行
|
||||
const retryOperation = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要重新执行该停机操作吗?`, '操作确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('重新执行成功')
|
||||
getShutdownList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'totalCount',
|
||||
label: '总数',
|
||||
formatter: (row) => `${row.totalCount} `
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '时间'
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功',
|
||||
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: 'success' }, () => `${row.successCount} 张`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureCount',
|
||||
label: '失败',
|
||||
|
||||
formatter: (row) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.failureCount > 0 ? 'danger' : 'success' },
|
||||
() => `${row.failureCount} 张`
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operationType',
|
||||
label: '操作类型',
|
||||
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getOperationTagType(row.operationType) }, () => row.operationType)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
onClick: () => retryOperation(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getShutdownList()
|
||||
})
|
||||
|
||||
// 获取停机管理列表
|
||||
const getShutdownList = 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
|
||||
} catch (error) {
|
||||
console.error('获取停机管理列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getShutdownList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getShutdownList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getShutdownList()
|
||||
}
|
||||
|
||||
// 显示停机设置对话框
|
||||
const showShutdownDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.shutdownType = ''
|
||||
formData.shutdownTime = ''
|
||||
formData.reason = ''
|
||||
formData.affectedCards = 1
|
||||
formData.notifyMethod = []
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
shutdownType: [{ required: true, message: '请选择停机类型', trigger: 'change' }],
|
||||
shutdownTime: [{ required: true, message: '请选择停机时间', trigger: 'change' }],
|
||||
reason: [
|
||||
{ required: true, message: '请输入停机原因', trigger: 'blur' },
|
||||
{ min: 5, max: 200, message: '停机原因长度在 5 到 200 个字符', trigger: 'blur' }
|
||||
],
|
||||
affectedCards: [{ required: true, message: '请输入影响的网卡数量', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 如果是定时停机,验证停机时间
|
||||
if (formData.shutdownType === 'scheduled' && !formData.shutdownTime) {
|
||||
ElMessage.error('定时停机必须选择停机时间')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`停机设置成功!类型:${formData.shutdownType},影响网卡:${formData.affectedCards} 张`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getShutdownList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-shutdown-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,739 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="card-transfer-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showBatchTransferDialog">批量转卡</ElButton>
|
||||
<ElButton @click="exportData">导出</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="批量转卡"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="转卡类型" prop="transferType">
|
||||
<ElSelect
|
||||
v-model="formData.transferType"
|
||||
placeholder="请选择转卡类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="运营商转接" value="operator_transfer" />
|
||||
<ElOption label="套餐转换" value="package_transfer" />
|
||||
<ElOption label="紧急转接" value="emergency_transfer" />
|
||||
<ElOption label="升级转接" value="upgrade_transfer" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标运营商" prop="targetOperator">
|
||||
<ElSelect
|
||||
v-model="formData.targetOperator"
|
||||
placeholder="请选择目标运营商"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="operator in operatorOptions"
|
||||
:key="operator.value"
|
||||
:label="operator.label"
|
||||
:value="operator.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="目标套餐" prop="targetPackage">
|
||||
<ElSelect
|
||||
v-model="formData.targetPackage"
|
||||
placeholder="请选择目标套餐"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.value"
|
||||
:label="pkg.label"
|
||||
:value="pkg.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="生效时间" prop="effectiveTime">
|
||||
<ElDatePicker
|
||||
v-model="formData.effectiveTime"
|
||||
type="datetime"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否保留流量" prop="keepTraffic">
|
||||
<ElRadioGroup v-model="formData.keepTraffic">
|
||||
<ElRadio :label="true">保留剩余流量</ElRadio>
|
||||
<ElRadio :label="false">清零重新计算</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="转卡原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="formData.reason"
|
||||
type="textarea"
|
||||
placeholder="请输入转卡原因"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认转卡
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElDatePicker,
|
||||
ElRadioGroup,
|
||||
ElRadio
|
||||
} from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'CardTransfer' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
oldOperator: '',
|
||||
transferType: '',
|
||||
iccid: '',
|
||||
transferDateRange: '',
|
||||
newOperator: '',
|
||||
status: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
transferType: '',
|
||||
targetOperator: '',
|
||||
targetPackage: '',
|
||||
effectiveTime: '',
|
||||
keepTraffic: true,
|
||||
reason: ''
|
||||
})
|
||||
|
||||
// 运营商选项
|
||||
const operatorOptions = ref([
|
||||
{ label: '中国移动', value: 'mobile' },
|
||||
{ label: '中国联通', value: 'unicom' },
|
||||
{ label: '中国电信', value: 'telecom' },
|
||||
{ label: 'GS移动', value: 'gs_mobile' },
|
||||
{ label: 'DC物联', value: 'dc_iot' },
|
||||
{ label: 'GDWL', value: 'gdwl' },
|
||||
{ label: 'GS联通', value: 'gs_unicom' },
|
||||
{ label: 'GS电信', value: 'gs_telecom' }
|
||||
])
|
||||
|
||||
// 套餐选项
|
||||
const packageOptions = ref([
|
||||
{ label: '随意联畅玩年卡套餐(12个月)', value: 'package1' },
|
||||
{ label: '如意包年3G流量包', value: 'package2' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package3' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package4' },
|
||||
{ label: '广电飞悦卡无预存50G(30天)', value: 'package5' },
|
||||
{ label: '5G畅享套餐', value: 'package6' },
|
||||
{ label: '移动物联网专用套餐', value: 'package7' },
|
||||
{ label: '电信天翼套餐', value: 'package8' }
|
||||
])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
oldCardNumber: '89860621370079892035',
|
||||
oldAccessNumber: '1440012345678',
|
||||
newCardNumber: '89860621370079892135',
|
||||
newAccessNumber: '1440012345778',
|
||||
transferType: '运营商转接',
|
||||
oldOperator: '中国移动',
|
||||
newOperator: '中国联通',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
totalTraffic: '100GB',
|
||||
usedTraffic: '25.5GB',
|
||||
transferTime: '2025-11-08 10:30:00',
|
||||
operator: '张若暄',
|
||||
status: '转接成功'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
oldCardNumber: '89860621370079892036',
|
||||
oldAccessNumber: '1440012345679',
|
||||
newCardNumber: '89860621370079892136',
|
||||
newAccessNumber: '1440012345779',
|
||||
transferType: '套餐转换',
|
||||
oldOperator: '中国联通',
|
||||
newOperator: '中国联通',
|
||||
packageName: '如意包年3G流量包',
|
||||
totalTraffic: '50GB',
|
||||
usedTraffic: '12.8GB',
|
||||
transferTime: '2025-11-07 14:15:00',
|
||||
operator: '孔丽娟',
|
||||
status: '转接进行中'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
oldCardNumber: '89860621370079892037',
|
||||
oldAccessNumber: '1440012345680',
|
||||
newCardNumber: '89860621370079892137',
|
||||
newAccessNumber: '1440012345780',
|
||||
transferType: '紧急转接',
|
||||
oldOperator: 'GS移动',
|
||||
newOperator: '中国移动',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
totalTraffic: '30GB',
|
||||
usedTraffic: '28.9GB',
|
||||
transferTime: '2025-11-06 09:45:00',
|
||||
operator: '李佳音',
|
||||
status: '转接失败'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
oldCardNumber: '89860621370079892038',
|
||||
oldAccessNumber: '1440012345681',
|
||||
newCardNumber: '89860621370079892138',
|
||||
newAccessNumber: '1440012345781',
|
||||
transferType: '升级转接',
|
||||
oldOperator: '中国电信',
|
||||
newOperator: '中国电信',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
totalTraffic: '100GB',
|
||||
usedTraffic: '0GB',
|
||||
transferTime: '2025-11-05 16:20:00',
|
||||
operator: '赵强',
|
||||
status: '等待确认'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
oldCardNumber: '89860621370079892039',
|
||||
oldAccessNumber: '1440012345682',
|
||||
newCardNumber: '89860621370079892139',
|
||||
newAccessNumber: '1440012345782',
|
||||
transferType: '运营商转接',
|
||||
oldOperator: 'GDWL',
|
||||
newOperator: '中国联通',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
totalTraffic: '50GB',
|
||||
usedTraffic: '35.2GB',
|
||||
transferTime: '2025-11-04 11:30:00',
|
||||
operator: '张若暄',
|
||||
status: '转接成功'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '老卡运营商',
|
||||
prop: 'oldOperator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => operatorOptions.value,
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '类型',
|
||||
prop: 'transferType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '运营商转接', value: 'operator_transfer' },
|
||||
{ label: '套餐转换', value: 'package_transfer' },
|
||||
{ label: '紧急转接', value: 'emergency_transfer' },
|
||||
{ label: '升级转接', value: 'upgrade_transfer' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '转卡时间',
|
||||
prop: 'transferDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '新卡运营商',
|
||||
prop: 'newOperator',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => operatorOptions.value,
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '转接成功', value: 'success' },
|
||||
{ label: '转接进行中', value: 'in_progress' },
|
||||
{ label: '转接失败', value: 'failed' },
|
||||
{ label: '等待确认', value: 'pending' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '老卡卡号', prop: 'oldCardNumber' },
|
||||
{ label: '老卡接入号', prop: 'oldAccessNumber' },
|
||||
{ label: '新卡卡号', prop: 'newCardNumber' },
|
||||
{ label: '新卡接入号', prop: 'newAccessNumber' },
|
||||
{ label: '类型', prop: 'transferType' },
|
||||
{ label: '老卡运营商', prop: 'oldOperator' },
|
||||
{ label: '新卡运营商', prop: 'newOperator' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '总流量', prop: 'totalTraffic' },
|
||||
{ label: '转卡已使用流量', prop: 'usedTraffic' },
|
||||
{ label: '转卡时间', prop: 'transferTime' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '转接成功':
|
||||
return 'success'
|
||||
case '转接进行中':
|
||||
return 'warning'
|
||||
case '转接失败':
|
||||
return 'danger'
|
||||
case '等待确认':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取转卡类型标签类型
|
||||
const getTransferTypeTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case '运营商转接':
|
||||
return 'primary'
|
||||
case '套餐转换':
|
||||
return 'success'
|
||||
case '紧急转接':
|
||||
return 'danger'
|
||||
case '升级转接':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条转卡记录`)
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看转卡详情: ${row.oldCardNumber} -> ${row.newCardNumber}`)
|
||||
}
|
||||
|
||||
// 取消转卡
|
||||
const cancelTransfer = (row: any) => {
|
||||
if (row.status === '转接成功') {
|
||||
ElMessage.warning('已成功转接的记录无法取消')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要取消该转卡操作吗?`, '取消确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('转卡操作已取消')
|
||||
getCardTransferList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 重新转卡
|
||||
const retryTransfer = (row: any) => {
|
||||
if (row.status !== '转接失败') {
|
||||
ElMessage.warning('只有失败的记录才能重新转卡')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要重新执行转卡操作吗?`, '操作确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('重新转卡操作已开始')
|
||||
getCardTransferList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'oldCardNumber',
|
||||
label: '老卡卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'oldAccessNumber',
|
||||
label: '老卡接入号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'newCardNumber',
|
||||
label: '新卡卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'newAccessNumber',
|
||||
label: '新卡接入号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'transferType',
|
||||
label: '类型',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTransferTypeTagType(row.transferType) }, () => row.transferType)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'oldOperator',
|
||||
label: '老卡运营商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'newOperator',
|
||||
label: '新卡运营商',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'totalTraffic',
|
||||
label: '总流量',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'usedTraffic',
|
||||
label: '转卡已使用流量',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'transferTime',
|
||||
label: '转卡时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
disabled: row.status !== '转接失败',
|
||||
onClick: () => retryTransfer(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '取消',
|
||||
disabled: row.status === '转接成功',
|
||||
onClick: () => cancelTransfer(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCardTransferList()
|
||||
})
|
||||
|
||||
// 获取网卡转接列表
|
||||
const getCardTransferList = 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
|
||||
} catch (error) {
|
||||
console.error('获取网卡转接列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCardTransferList()
|
||||
}
|
||||
|
||||
// 显示批量转卡对话框
|
||||
const showBatchTransferDialog = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要转卡的记录')
|
||||
return
|
||||
}
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.transferType = ''
|
||||
formData.targetOperator = ''
|
||||
formData.targetPackage = ''
|
||||
formData.effectiveTime = ''
|
||||
formData.keepTraffic = true
|
||||
formData.reason = ''
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
transferType: [{ required: true, message: '请选择转卡类型', trigger: 'change' }],
|
||||
targetOperator: [{ required: true, message: '请选择目标运营商', trigger: 'change' }],
|
||||
targetPackage: [{ required: true, message: '请选择目标套餐', trigger: 'change' }],
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
|
||||
reason: [
|
||||
{ required: true, message: '请输入转卡原因', trigger: 'blur' },
|
||||
{ min: 5, max: 200, message: '转卡原因长度在 5 到 200 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
const targetOperatorName = operatorOptions.value.find(
|
||||
(op) => op.value === formData.targetOperator
|
||||
)?.label
|
||||
const targetPackageName = packageOptions.value.find(
|
||||
(pkg) => pkg.value === formData.targetPackage
|
||||
)?.label
|
||||
ElMessage.success(
|
||||
`批量转卡操作提交成功!目标运营商:${targetOperatorName},目标套餐:${targetPackageName},影响${selectedRows.value.length}张网卡`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getCardTransferList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-transfer-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,699 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="my-cards-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="exportExcel">导出excel</ElButton>
|
||||
<ElButton @click="cardDistribution">网卡分销</ElButton>
|
||||
<ElButton @click="batchRecharge">批量充值</ElButton>
|
||||
<ElButton @click="cardRecycle">网卡回收</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 网卡分销弹框 -->
|
||||
<CardOperationDialog
|
||||
v-model:visible="distributionDialogVisible"
|
||||
title="网卡分销"
|
||||
select-label="代理商"
|
||||
select-placeholder="请搜索并选择代理商"
|
||||
:selected-cards="selectedRows"
|
||||
:remote-search="searchAgents"
|
||||
@confirm="handleDistributionConfirm"
|
||||
@close="handleDialogClose"
|
||||
/>
|
||||
|
||||
<!-- 批量充值弹框 -->
|
||||
<CardOperationDialog
|
||||
v-model:visible="rechargeDialogVisible"
|
||||
title="批量充值"
|
||||
select-label="套餐"
|
||||
select-placeholder="请搜索并选择套餐"
|
||||
:show-amount="true"
|
||||
:selected-cards="selectedRows"
|
||||
:remote-search="searchPackages"
|
||||
@confirm="handleRechargeConfirm"
|
||||
@close="handleDialogClose"
|
||||
/>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
import CardOperationDialog from '@/components/business/CardOperationDialog.vue'
|
||||
|
||||
defineOptions({ name: 'MyCards' })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 弹框状态
|
||||
const distributionDialogVisible = ref(false)
|
||||
const rechargeDialogVisible = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
distributor: '',
|
||||
cardStatus: '',
|
||||
importDateRange: '',
|
||||
cardCompany: '',
|
||||
virtualNumber: '',
|
||||
iccid: '',
|
||||
iccidRange: '',
|
||||
cardPackage: '',
|
||||
virtualNumberRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
cardCompany: '联通2',
|
||||
iccid: '89860621370079892035',
|
||||
virtualNumber: '10655001234',
|
||||
expireTime: '2025-12-31',
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
distributorName: '张丽丽',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cardCompany: '联通36',
|
||||
iccid: '89860621370079892036',
|
||||
virtualNumber: '10655001235',
|
||||
expireTime: '2025-11-30',
|
||||
packageName: '如意包年3G流量包',
|
||||
distributorName: '王小明',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cardCompany: 'SXKJ-NB',
|
||||
iccid: '89860621370079892037',
|
||||
virtualNumber: '10655001236',
|
||||
expireTime: '2025-10-31',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
distributorName: 'HNSXKJ',
|
||||
cardStatus: '待激活'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cardCompany: '联通1-1',
|
||||
iccid: '89860621370079892038',
|
||||
virtualNumber: '10655001237',
|
||||
expireTime: '2026-01-31',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
distributorName: '未分销',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cardCompany: '广电4',
|
||||
iccid: '89860621370079892039',
|
||||
virtualNumber: '10655001238',
|
||||
expireTime: '2025-08-31',
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
distributorName: '孔丽娟',
|
||||
cardStatus: '停机'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
cardCompany: '联通8',
|
||||
iccid: '89860621370079892040',
|
||||
virtualNumber: '10655001239',
|
||||
expireTime: '2025-09-15',
|
||||
packageName: '5G畅享套餐',
|
||||
distributorName: '李佳音',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
cardCompany: '移动21',
|
||||
iccid: '89860621370079892041',
|
||||
virtualNumber: '10655001240',
|
||||
expireTime: '2025-07-20',
|
||||
packageName: '移动物联网专用套餐',
|
||||
distributorName: '赵强',
|
||||
cardStatus: '已过期'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
cardCompany: '电信9',
|
||||
iccid: '89860621370079892042',
|
||||
virtualNumber: '10655001241',
|
||||
expireTime: '2026-02-28',
|
||||
packageName: '电信天翼套餐',
|
||||
distributorName: '未分销',
|
||||
cardStatus: '正常'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入分销商'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '网卡状态',
|
||||
prop: 'cardStatus',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '正常', value: 'normal' },
|
||||
{ label: '待激活', value: 'pending' },
|
||||
{ label: '停机', value: 'suspended' },
|
||||
{ label: '已过期', value: 'expired' },
|
||||
{ label: '注销', value: 'cancelled' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '导入时间',
|
||||
prop: 'importDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号',
|
||||
prop: 'virtualNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID号',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID号段',
|
||||
prop: 'iccidRange',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号段,如:8986001-8986999'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '卡套餐',
|
||||
prop: 'cardPackage',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '随意联畅玩年卡套餐', value: 'package1' },
|
||||
{ label: '如意包年3G流量包', value: 'package2' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package3' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package4' },
|
||||
{ label: '广电飞悦卡无预存50G', value: 'package5' },
|
||||
{ label: '5G畅享套餐', value: 'package6' },
|
||||
{ label: '移动物联网专用套餐', value: 'package7' },
|
||||
{ label: '电信天翼套餐', value: 'package8' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号段',
|
||||
prop: 'virtualNumberRange',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号段,如:10655001-10655999'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: 'ICCID卡号', prop: 'iccid' },
|
||||
{ label: '虚拟号', prop: 'virtualNumber' },
|
||||
{ label: '到期时间', prop: 'expireTime' },
|
||||
{ label: '套餐', prop: 'packageName' },
|
||||
{ label: '分销商姓名', prop: 'distributorName' },
|
||||
{ label: '卡状态', prop: 'cardStatus' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取卡状态标签类型
|
||||
const getCardStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '正常':
|
||||
return 'success'
|
||||
case '待激活':
|
||||
return 'warning'
|
||||
case '停机':
|
||||
case '注销':
|
||||
return 'danger'
|
||||
case '已过期':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条记录到Excel`)
|
||||
}
|
||||
|
||||
// 网卡分销
|
||||
const cardDistribution = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要分销的网卡')
|
||||
return
|
||||
}
|
||||
distributionDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 批量充值
|
||||
const batchRecharge = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要充值的网卡')
|
||||
return
|
||||
}
|
||||
rechargeDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 网卡回收
|
||||
const cardRecycle = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要回收的网卡')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确定要回收所选的 ${selectedRows.value.length} 张网卡吗?回收后将无法恢复。`,
|
||||
'网卡回收确认',
|
||||
{
|
||||
confirmButtonText: '确认回收',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: false
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
handleCardRecycle()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消回收操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看网卡 ${row.iccid} 的详细信息`)
|
||||
}
|
||||
|
||||
// 编辑网卡
|
||||
const editCard = (row: any) => {
|
||||
ElMessage.info(`编辑网卡 ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 删除网卡
|
||||
const deleteCard = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除ICCID为 ${row.iccid} 的网卡吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success(`已删除网卡 ${row.iccid}`)
|
||||
getMyCardsList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'virtualNumber',
|
||||
label: '虚拟号',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'expireTime',
|
||||
label: '到期时间',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '分销商姓名',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'cardStatus',
|
||||
label: '卡状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getCardStatusType(row.cardStatus) }, () => row.cardStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editCard(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteCard(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getMyCardsList()
|
||||
})
|
||||
|
||||
// 获取我的网卡列表
|
||||
const getMyCardsList = 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
|
||||
} catch (error) {
|
||||
console.error('获取我的网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getMyCardsList()
|
||||
}
|
||||
|
||||
// 远程搜索代理商
|
||||
const searchAgents = async (query: string) => {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// 模拟代理商数据
|
||||
const allAgents = [
|
||||
{ label: '张丽丽代理商', value: 'agent_001' },
|
||||
{ label: '王小明代理商', value: 'agent_002' },
|
||||
{ label: 'HNSXKJ代理商', value: 'agent_003' },
|
||||
{ label: '孔丽娟代理商', value: 'agent_004' },
|
||||
{ label: '李四代理商', value: 'agent_005' },
|
||||
{ label: '赵六代理商', value: 'agent_006' },
|
||||
{ label: '刘备代理商', value: 'agent_007' },
|
||||
{ label: '关羽代理商', value: 'agent_008' },
|
||||
{ label: '张飞代理商', value: 'agent_009' },
|
||||
{ label: '赵云代理商', value: 'agent_010' },
|
||||
{ label: '黄忠代理商', value: 'agent_011' },
|
||||
{ label: '马超代理商', value: 'agent_012' },
|
||||
{ label: '诸葛亮代理商', value: 'agent_013' },
|
||||
{ label: '周瑜代理商', value: 'agent_014' },
|
||||
{ label: '孙权代理商', value: 'agent_015' }
|
||||
]
|
||||
|
||||
// 根据查询条件过滤
|
||||
let filteredAgents = allAgents
|
||||
if (query) {
|
||||
filteredAgents = allAgents.filter((agent) =>
|
||||
agent.label.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// 返回前10条数据
|
||||
return filteredAgents.slice(0, 10)
|
||||
}
|
||||
|
||||
// 远程搜索套餐
|
||||
const searchPackages = async (query: string) => {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// 模拟套餐数据
|
||||
const allPackages = [
|
||||
{ label: '随意联畅玩年卡套餐(12个月)', value: 'package_001' },
|
||||
{ label: '如意包年3G流量包', value: 'package_002' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package_003' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package_004' },
|
||||
{ label: '广电飞悦卡无预存50G(30天)', value: 'package_005' },
|
||||
{ label: '移动畅享套餐50G', value: 'package_006' },
|
||||
{ label: '联通大王卡19元', value: 'package_007' },
|
||||
{ label: '电信星卡29元', value: 'package_008' },
|
||||
{ label: '移动无限流量卡99元', value: 'package_009' },
|
||||
{ label: '联通冰淇淋套餐199元', value: 'package_010' },
|
||||
{ label: '电信天翼云卡59元', value: 'package_011' },
|
||||
{ label: '移动神州行卡39元', value: 'package_012' },
|
||||
{ label: '联通腾讯王卡免流', value: 'package_013' },
|
||||
{ label: '电信达量降速套餐', value: 'package_014' },
|
||||
{ label: '广电5G畅享套餐', value: 'package_015' }
|
||||
]
|
||||
|
||||
// 根据查询条件过滤
|
||||
let filteredPackages = allPackages
|
||||
if (query) {
|
||||
filteredPackages = allPackages.filter((pkg) =>
|
||||
pkg.label.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// 返回前10条数据
|
||||
return filteredPackages.slice(0, 10)
|
||||
}
|
||||
|
||||
// 处理网卡分销确认
|
||||
const handleDistributionConfirm = async (data: any) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(`成功将 ${data.selectedCards.length} 张网卡分销给代理商`)
|
||||
distributionDialogVisible.value = false
|
||||
|
||||
// 刷新列表
|
||||
getMyCardsList()
|
||||
} catch (error) {
|
||||
console.log('分销操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理批量充值确认
|
||||
const handleRechargeConfirm = async (data: any) => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(`成功为 ${data.selectedCards.length} 张网卡充值 ${data.amount} 元`)
|
||||
rechargeDialogVisible.value = false
|
||||
|
||||
// 刷新列表
|
||||
getMyCardsList()
|
||||
} catch (error) {
|
||||
console.log('充值操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理网卡回收
|
||||
const handleCardRecycle = async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(`成功回收 ${selectedRows.value.length} 张网卡`)
|
||||
|
||||
// 从列表中移除回收的网卡
|
||||
const recycledIds = selectedRows.value.map((row) => row.id)
|
||||
tableData.value = tableData.value.filter((item) => !recycledIds.includes(item.id))
|
||||
selectedRows.value = []
|
||||
} catch (error) {
|
||||
console.log('回收操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理弹框关闭
|
||||
const handleDialogClose = () => {
|
||||
// 可以在这里添加额外的关闭逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-cards-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,705 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="offline-batch-recharge-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 数据视图组件 -->
|
||||
<ArtDataViewer
|
||||
ref="dataViewerRef"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:table-columns="columns"
|
||||
:descriptions-fields="descriptionsFields"
|
||||
:descriptions-columns="2"
|
||||
:pagination="pagination"
|
||||
:label-width="'120px'"
|
||||
:field-columns="columnChecks"
|
||||
:show-card-actions="true"
|
||||
:show-card-selection="true"
|
||||
:default-view="currentView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@view-change="handleViewChange"
|
||||
>
|
||||
<template #header-left>
|
||||
<ElButton type="primary" @click="handleSearch">搜索查询</ElButton>
|
||||
<ElButton type="success" @click="showBatchRechargeDialog">批量充值</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:currentView="currentView"
|
||||
@refresh="handleRefresh"
|
||||
@viewChange="handleViewChange"
|
||||
:show-title="false"
|
||||
:show-view-toggle="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ item }">
|
||||
<ArtButtonTable text="查看" @click="viewDetails(item)" />
|
||||
<ArtButtonTable text="重试" @click="retryRecharge(item)" />
|
||||
<ArtButtonTable text="删除" @click="deleteRecord(item)" />
|
||||
</template>
|
||||
</ArtDataViewer>
|
||||
|
||||
<!-- 批量充值对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="批量充值"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="选择套餐" prop="packageId">
|
||||
<ElSelect
|
||||
v-model="formData.packageId"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.value"
|
||||
:label="pkg.label"
|
||||
:value="pkg.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="生效方式" prop="effectiveMethod">
|
||||
<ElSelect
|
||||
v-model="formData.effectiveMethod"
|
||||
placeholder="请选择生效方式"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption
|
||||
v-for="method in effectiveMethods"
|
||||
:key="method.value"
|
||||
:label="method.label"
|
||||
:value="method.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
label="生效时间"
|
||||
prop="effectiveTime"
|
||||
v-if="formData.effectiveMethod === 'scheduled'"
|
||||
>
|
||||
<ElDatePicker
|
||||
v-model="formData.effectiveTime"
|
||||
type="datetime"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认充值
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElDatePicker,
|
||||
ElDivider,
|
||||
ElButton
|
||||
} from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'OfflineBatchRecharge' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 当前视图模式
|
||||
const currentView = ref('table')
|
||||
|
||||
// 数据视图组件引用
|
||||
const dataViewerRef = ref()
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
importDateRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
excelFile: null as File | null,
|
||||
packageId: '',
|
||||
effectiveMethod: '',
|
||||
effectiveTime: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 套餐选项
|
||||
const packageOptions = ref([
|
||||
{ label: '随意联畅玩年卡套餐(12个月)', value: 'package1' },
|
||||
{ label: '如意包年3G流量包', value: 'package2' },
|
||||
{ label: 'Y-NB专享套餐', value: 'package3' },
|
||||
{ label: '100G全国流量月卡套餐', value: 'package4' },
|
||||
{ label: '广电飞悦卡无预存50G(30天)', value: 'package5' },
|
||||
{ label: '5G畅享套餐', value: 'package6' },
|
||||
{ label: '移动物联网专用套餐', value: 'package7' },
|
||||
{ label: '电信天翼套餐', value: 'package8' }
|
||||
])
|
||||
|
||||
// 生效方式选项
|
||||
const effectiveMethods = ref([
|
||||
{ label: '立即生效', value: 'immediate' },
|
||||
{ label: '定时生效', value: 'scheduled' },
|
||||
{ label: '次月生效', value: 'next_month' },
|
||||
{ label: '手动激活', value: 'manual' }
|
||||
])
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccidStart: '89860621370079892001',
|
||||
iccidEnd: '89860621370079892100',
|
||||
importTime: '2025-11-08 10:30:00',
|
||||
importCount: 100,
|
||||
successCount: 98,
|
||||
failureCount: 2,
|
||||
packageName: '随意联畅玩年卡套餐(12个月)',
|
||||
packageCount: 100,
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccidStart: '89860621370079892101',
|
||||
iccidEnd: '89860621370079892200',
|
||||
importTime: '2025-11-07 14:15:00',
|
||||
importCount: 100,
|
||||
successCount: 100,
|
||||
failureCount: 0,
|
||||
packageName: '如意包年3G流量包',
|
||||
packageCount: 100,
|
||||
operator: '孔丽娟'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccidStart: '89860621370079892201',
|
||||
iccidEnd: '89860621370079892250',
|
||||
importTime: '2025-11-06 09:45:00',
|
||||
importCount: 50,
|
||||
successCount: 45,
|
||||
failureCount: 5,
|
||||
packageName: 'Y-NB专享套餐',
|
||||
packageCount: 50,
|
||||
operator: '李佳音'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccidStart: '89860621370079892251',
|
||||
iccidEnd: '89860621370079892350',
|
||||
importTime: '2025-11-05 16:20:00',
|
||||
importCount: 100,
|
||||
successCount: 97,
|
||||
failureCount: 3,
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
packageCount: 100,
|
||||
operator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccidStart: '89860621370079892351',
|
||||
iccidEnd: '89860621370079892400',
|
||||
importTime: '2025-11-04 11:30:00',
|
||||
importCount: 50,
|
||||
successCount: 50,
|
||||
failureCount: 0,
|
||||
packageName: '广电飞悦卡无预存50G(30天)',
|
||||
packageCount: 50,
|
||||
operator: '赵强'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '导入时间',
|
||||
prop: 'importDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID开始号段', prop: 'iccidStart' },
|
||||
{ label: 'ICCID结束号段', prop: 'iccidEnd' },
|
||||
{ label: '导入时间', prop: 'importTime' },
|
||||
{ label: '导入数量', prop: 'importCount' },
|
||||
{ label: '成功数量', prop: 'successCount' },
|
||||
{ label: '失败数量', prop: 'failureCount' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '套餐数量', prop: 'packageCount' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 描述字段配置
|
||||
const descriptionsFields = [
|
||||
{ prop: 'iccidStart', label: 'ICCID开始号段' },
|
||||
{ prop: 'iccidEnd', label: 'ICCID结束号段' },
|
||||
{ prop: 'importTime', label: '导入时间' },
|
||||
{ prop: 'importCount', label: '导入数量', formatter: (row: any) => `${row.importCount} 张` },
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功数量',
|
||||
formatter: (row: any) => {
|
||||
return `<el-tag type="success" size="small">${row.successCount} 张</el-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureCount',
|
||||
label: '失败数量',
|
||||
formatter: (row: any) => {
|
||||
const type = row.failureCount > 0 ? 'danger' : 'success'
|
||||
return `<el-tag type="${type}" size="small">${row.failureCount} 张</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'packageName', label: '套餐名称', span: 2 },
|
||||
{ prop: 'packageCount', label: '套餐数量', formatter: (row: any) => `${row.packageCount} 个` },
|
||||
{ prop: 'operator', label: '操作人' }
|
||||
]
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条记录到Excel`)
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载充值模板文件...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看充值记录详情: ${row.iccidStart} - ${row.iccidEnd}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该充值记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getOfflineBatchRechargeList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 重新充值
|
||||
const retryRecharge = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要重新执行该批次的充值操作吗?`, '操作确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('重新充值操作已开始')
|
||||
getOfflineBatchRechargeList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccidStart',
|
||||
label: 'ICCID开始号段',
|
||||
minWidth: 220
|
||||
},
|
||||
{
|
||||
prop: 'iccidEnd',
|
||||
label: 'ICCID结束号段',
|
||||
minWidth: 220
|
||||
},
|
||||
{
|
||||
prop: 'importTime',
|
||||
label: '导入时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'importCount',
|
||||
label: '导入数量',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.importCount} 张`
|
||||
},
|
||||
{
|
||||
prop: 'successCount',
|
||||
label: '成功数量',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: 'success' }, () => `${row.successCount} 张`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureCount',
|
||||
label: '失败数量',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: row.failureCount > 0 ? 'danger' : 'success' },
|
||||
() => `${row.failureCount} 张`
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'packageCount',
|
||||
label: '套餐数量',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.packageCount} 个`
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
onClick: () => retryRecharge(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getOfflineBatchRechargeList()
|
||||
})
|
||||
|
||||
// 获取线下批量充值列表
|
||||
const getOfflineBatchRechargeList = 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
|
||||
} catch (error) {
|
||||
console.error('获取线下批量充值列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getOfflineBatchRechargeList()
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
const handleViewChange = (view: string) => {
|
||||
console.log('视图切换到:', view)
|
||||
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
|
||||
}
|
||||
|
||||
// 显示批量充值对话框
|
||||
const showBatchRechargeDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.excelFile = null
|
||||
formData.packageId = ''
|
||||
formData.effectiveMethod = ''
|
||||
formData.effectiveTime = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
formData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }],
|
||||
packageId: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||
effectiveMethod: [{ required: true, message: '请选择生效方式', trigger: 'change' }],
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!formData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是定时生效,验证生效时间
|
||||
if (formData.effectiveMethod === 'scheduled' && !formData.effectiveTime) {
|
||||
ElMessage.error('定时生效必须选择生效时间')
|
||||
return
|
||||
}
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
const packageName = packageOptions.value.find(
|
||||
(p) => p.value === formData.packageId
|
||||
)?.label
|
||||
const effectiveMethodName = effectiveMethods.value.find(
|
||||
(m) => m.value === formData.effectiveMethod
|
||||
)?.label
|
||||
ElMessage.success(
|
||||
`批量充值提交成功!套餐:${packageName},生效方式:${effectiveMethodName}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getOfflineBatchRechargeList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.offline-batch-recharge-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,689 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-gift-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showBatchImportDialog">批量导入</ElButton>
|
||||
<ElButton type="danger" @click="batchDelete">批量删除</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="importDialogVisible"
|
||||
title="批量导入套餐赠送"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm
|
||||
ref="importFormRef"
|
||||
:model="importFormData"
|
||||
:rules="importRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="importFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
|
||||
确认导入
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElUpload, ElIcon, ElDivider } from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageGift' })
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const importLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
iccid: '',
|
||||
accessNumber: '',
|
||||
cardCompany: '',
|
||||
isReceived: '',
|
||||
endDateRange: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 导入表单实例
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 导入表单数据
|
||||
const importFormData = reactive({
|
||||
excelFile: null as File | null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccid: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
giftPackage: '随意联畅玩年卡套餐(12个月)',
|
||||
cardCompany: '联通2',
|
||||
isReceived: '已领取',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00',
|
||||
receiveTime: '2025-11-08 14:20:00',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccid: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
giftPackage: 'Y-NB专享套餐',
|
||||
cardCompany: 'SXKJ-NB',
|
||||
isReceived: '未领取',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00',
|
||||
receiveTime: '',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccid: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
giftPackage: '如意包年3G流量包',
|
||||
cardCompany: '联通36',
|
||||
isReceived: '已过期',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00',
|
||||
receiveTime: '',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccid: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
giftPackage: '100G全国流量月卡套餐',
|
||||
cardCompany: '联通1-1',
|
||||
isReceived: '未领取',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00',
|
||||
receiveTime: '',
|
||||
importStatus: '导入失败',
|
||||
failureReason: 'ICCID格式错误'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccid: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
giftPackage: '广电飞悦卡无预存50G(30天)',
|
||||
cardCompany: '广电4',
|
||||
isReceived: '已领取',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00',
|
||||
receiveTime: '2025-11-05 08:15:00',
|
||||
importStatus: '导入成功',
|
||||
failureReason: ''
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: 'ICCID号',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '接入号',
|
||||
prop: 'accessNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入接入号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通2', value: 'unicom2' },
|
||||
{ label: '联通36', value: 'unicom36' },
|
||||
{ label: 'SXKJ-NB', value: 'sxkj_nb' },
|
||||
{ label: '联通1-1', value: 'unicom1_1' },
|
||||
{ label: '联通8', value: 'unicom8' },
|
||||
{ label: '移动21', value: 'mobile21' },
|
||||
{ label: '广电4', value: 'gdtv4' },
|
||||
{ label: '电信9', value: 'telecom9' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '是否领取',
|
||||
prop: 'isReceived',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '已领取', value: 'received' },
|
||||
{ label: '未领取', value: 'not_received' },
|
||||
{ label: '已过期', value: 'expired' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '结束时间',
|
||||
prop: 'endDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID', prop: 'iccid' },
|
||||
{ label: '接入号码', prop: 'accessNumber' },
|
||||
{ label: '赠送套餐', prop: 'giftPackage' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '是否领取', prop: 'isReceived' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '领取时间', prop: 'receiveTime' },
|
||||
{ label: '导入状态', prop: 'importStatus' },
|
||||
{ label: '失败原因', prop: 'failureReason' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取是否领取标签类型
|
||||
const getReceiveStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '已领取':
|
||||
return 'success'
|
||||
case '未领取':
|
||||
return 'warning'
|
||||
case '已过期':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取导入状态标签类型
|
||||
const getImportStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '导入成功':
|
||||
return 'success'
|
||||
case '导入失败':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐赠送记录`)
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const batchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 条套餐赠送记录吗?`,
|
||||
'批量删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
ElMessage.success(`批量删除 ${selectedRows.value.length} 条记录成功`)
|
||||
getPackageGiftList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 显示导入对话框
|
||||
const showBatchImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (importFormRef.value) {
|
||||
importFormRef.value.resetFields()
|
||||
}
|
||||
importFormData.excelFile = null
|
||||
importFormData.remark = ''
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载套餐赠送导入模板...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看套餐赠送详情: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑套餐赠送记录: ${row.iccid}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除该套餐赠送记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getPackageGiftList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 手动发放
|
||||
const manualGrant = (row: any) => {
|
||||
if (row.isReceived === '已领取') {
|
||||
ElMessage.warning('该套餐已被领取')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要手动发放该套餐吗?`, '发放确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('套餐发放成功')
|
||||
getPackageGiftList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消发放')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号码',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'giftPackage',
|
||||
label: '赠送套餐',
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'isReceived',
|
||||
label: '是否领取',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getReceiveStatusType(row.isReceived) }, () => row.isReceived)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'receiveTime',
|
||||
label: '领取时间',
|
||||
width: 160,
|
||||
formatter: (row) => row.receiveTime || '未领取'
|
||||
},
|
||||
{
|
||||
prop: 'importStatus',
|
||||
label: '导入状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getImportStatusType(row.importStatus) }, () => row.importStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failureReason',
|
||||
label: '失败原因',
|
||||
width: 140,
|
||||
formatter: (row) => row.failureReason || '-'
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 280,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '发放',
|
||||
disabled: row.isReceived === '已领取',
|
||||
onClick: () => manualGrant(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageGiftList()
|
||||
})
|
||||
|
||||
// 获取套餐赠送列表
|
||||
const getPackageGiftList = 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
|
||||
} catch (error) {
|
||||
console.error('获取套餐赠送列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageGiftList()
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
importFormData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单验证规则
|
||||
const importRules = reactive<FormRules>({
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交导入
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importFormRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!importFormData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await importFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
importLoading.value = true
|
||||
|
||||
// 模拟导入过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success('套餐赠送导入成功!')
|
||||
importDialogVisible.value = false
|
||||
importLoading.value = false
|
||||
getPackageGiftList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-gift-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h3 class="table-title"><i class="iconfont-sys"></i>更新日志</h3>
|
||||
|
||||
<ArtTable :data="upgradeLogList" :pagination="false">
|
||||
<ElTableColumn label="版本号" prop="version" width="200" />
|
||||
<ElTableColumn label="内容">
|
||||
<template #default="scope">
|
||||
<div class="title">{{ scope.row.title }}</div>
|
||||
<div v-if="scope.row.detail" style="margin-top: 10px">
|
||||
<div class="detail-item" v-for="(item, index) in scope.row.detail" :key="index">
|
||||
{{ index + 1 }}. {{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="时间" prop="date" />
|
||||
</ArtTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtTable from '@/components/core/tables/ArtTable.vue'
|
||||
import { upgradeLogList } from '@/mock/upgrade/changeLog'
|
||||
|
||||
defineOptions({ name: 'ChangeLog' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.table-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -46,28 +46,15 @@
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
@@ -323,11 +310,7 @@
|
||||
} from '@/types/api/commission'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import {
|
||||
CommissionStatusMap,
|
||||
@@ -342,21 +325,10 @@
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 主表格状态
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const summaryList = ref<ShopCommissionSummaryItem[]>([])
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<ShopCommissionSummaryItem | null>(null)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
@@ -604,6 +576,21 @@
|
||||
loadCommissionRecords()
|
||||
}
|
||||
|
||||
// 获取操作按钮
|
||||
const getActions = (row: ShopCommissionSummaryItem) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('agent_commission:detail')) {
|
||||
actions.push({
|
||||
label: '查看详情',
|
||||
handler: () => showDetail(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// 监听tab切换
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'commission') {
|
||||
@@ -704,30 +691,6 @@
|
||||
loadWithdrawalRecords()
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
// 移除详情选项,通过点击店铺名称查看详情
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: ShopCommissionSummaryItem, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
// 暂无其他菜单项
|
||||
}
|
||||
|
||||
// 处理佣金修正
|
||||
const handleResolveCommission = (
|
||||
row: ShopCommissionRecordItem,
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="analysis-dashboard">
|
||||
<!-- 轮询统计 -->
|
||||
<div class="section-title">轮询统计</div>
|
||||
<PollingStats />
|
||||
|
||||
<!-- 佣金概览 -->
|
||||
<div class="section-title">佣金概览</div>
|
||||
<CommissionSummary />
|
||||
@@ -56,6 +60,7 @@
|
||||
import TopProducts from './widget/TopProducts.vue'
|
||||
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
|
||||
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
|
||||
import PollingStats from './widget/PollingStats.vue'
|
||||
import CommissionSummary from './widget/CommissionSummary.vue'
|
||||
import WithdrawalSettings from './widget/WithdrawalSettings.vue'
|
||||
import ActiveWechatConfig from './widget/ActiveWechatConfig.vue'
|
||||
|
||||
195
src/views/dashboard/analysis/widget/PollingStats.vue
Normal file
195
src/views/dashboard/analysis/widget/PollingStats.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<ElCard shadow="never" class="polling-stats-widget">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<span class="header-title">实时统计</span>
|
||||
<ElTag type="primary" effect="dark" size="small">实时</ElTag>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<ElButton size="small" @click="loadOverviewStats" :loading="loading">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)">
|
||||
<el-icon :size="24"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">总卡数</div>
|
||||
<div class="stat-value">{{ overviewStats?.total_cards?.toLocaleString() || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)">
|
||||
<el-icon :size="24"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已初始化</div>
|
||||
<div class="stat-value">
|
||||
{{ overviewStats?.initialized_cards?.toLocaleString() || 0 }} / {{ overviewStats?.total_cards?.toLocaleString() || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)">
|
||||
<el-icon :size="24"><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">实名队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.realname_queue_size?.toLocaleString() || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)">
|
||||
<el-icon :size="24"><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">套餐队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.package_queue_size?.toLocaleString() || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)">
|
||||
<el-icon :size="24"><DataLine /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">流量队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.carddata_queue_size?.toLocaleString() || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElTag, ElButton } from 'element-plus'
|
||||
import { Document, CircleCheck, User, Box, DataLine } from '@element-plus/icons-vue'
|
||||
import { PollingMonitorService } from '@/api/modules'
|
||||
import type { PollingOverviewStats } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'PollingStats' })
|
||||
|
||||
const overviewStats = ref<PollingOverviewStats>()
|
||||
const loading = ref(false)
|
||||
|
||||
// 加载总览统计
|
||||
const loadOverviewStats = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await PollingMonitorService.getOverviewStats()
|
||||
overviewStats.value = data
|
||||
} catch (error) {
|
||||
ElMessage.error('加载轮询统计失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOverviewStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.polling-stats-widget {
|
||||
:deep(.el-card__header) {
|
||||
padding: 18px 20px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: var(--el-fill-color-blank);
|
||||
border-radius: 8px;
|
||||
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.polling-stats-widget {
|
||||
.card-header {
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showCreateDialog">创建充值订单</ElButton>
|
||||
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('agent_recharge:create')">创建充值订单</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -32,29 +32,16 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 充值订单操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="rechargeOperationMenuRef"
|
||||
:menu-items="rechargeOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleRechargeOperationMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 创建充值订单对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
@@ -181,17 +168,15 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
defineOptions({ name: 'AgentRechargeList' })
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
@@ -201,10 +186,6 @@
|
||||
const confirmPayDialogVisible = ref(false)
|
||||
const currentRecharge = ref<AgentRecharge | null>(null)
|
||||
|
||||
// 右键菜单
|
||||
const rechargeOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingRecharge = ref<AgentRecharge | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: AgentRechargeQueryParams = {
|
||||
shop_id: undefined,
|
||||
@@ -609,71 +590,25 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (row: AgentRecharge) => {
|
||||
router.push({
|
||||
path: `${RoutesAlias.AgentRecharge}/detail/${row.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 充值订单操作菜单项配置
|
||||
const rechargeOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
// 获取操作按钮
|
||||
const getActions = (row: AgentRecharge) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 待支付且线下转账的订单可以确认支付
|
||||
if (
|
||||
currentOperatingRecharge.value?.status === 1 &&
|
||||
currentOperatingRecharge.value?.payment_method === 'offline'
|
||||
) {
|
||||
items.push({
|
||||
key: 'confirm_pay',
|
||||
label: '确认支付'
|
||||
if (row.status === 1 && row.payment_method === 'offline' && hasAuth('agent_recharge:confirm_payment')) {
|
||||
actions.push({
|
||||
label: '确认支付',
|
||||
handler: () => handleShowConfirmPay(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示充值订单操作右键菜单
|
||||
const showRechargeOperationMenu = (e: MouseEvent, row: AgentRecharge) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingRecharge.value = row
|
||||
rechargeOperationMenuRef.value?.show(e)
|
||||
return actions
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: AgentRecharge, column: any, event: MouseEvent) => {
|
||||
showRechargeOperationMenu(event, row)
|
||||
}
|
||||
|
||||
// 处理充值订单操作菜单选择
|
||||
const handleRechargeOperationMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentOperatingRecharge.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'confirm_pay':
|
||||
handleShowConfirmPay(currentOperatingRecharge.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-recharge-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElCard shadow="never">
|
||||
<!-- 搜索栏 -->
|
||||
<ElForm :inline="true" :model="searchForm" class="search-form">
|
||||
<ElFormItem label="客户账号">
|
||||
<ElInput
|
||||
v-model="searchForm.accountNo"
|
||||
placeholder="请输入客户账号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="客户名称">
|
||||
<ElInput
|
||||
v-model="searchForm.customerName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="客户类型">
|
||||
<ElSelect
|
||||
v-model="searchForm.customerType"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<ElOption label="代理商" value="agent" />
|
||||
<ElOption label="企业客户" value="enterprise" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton @click="handleReset">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<ArtTable :data="tableData" index stripe>
|
||||
<template #default>
|
||||
<ElTableColumn label="客户账号" prop="accountNo" min-width="150" />
|
||||
<ElTableColumn label="客户名称" prop="customerName" min-width="150" />
|
||||
<ElTableColumn label="客户类型" prop="customerType" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.customerType === 'agent' ? 'success' : 'primary'">
|
||||
{{ scope.row.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金总额" prop="totalCommission" width="150">
|
||||
<template #default="scope"> ¥{{ scope.row.totalCommission.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="可提现金额" prop="availableAmount" width="150">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-success)">
|
||||
¥{{ scope.row.availableAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="待入账金额" prop="pendingAmount" width="150">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-warning)">
|
||||
¥{{ scope.row.pendingAmount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已提现金额" prop="withdrawnAmount" width="150">
|
||||
<template #default="scope"> ¥{{ scope.row.withdrawnAmount.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="提现次数" prop="withdrawCount" width="100" />
|
||||
<ElTableColumn label="最后提现时间" prop="lastWithdrawTime" width="180" />
|
||||
<ElTableColumn label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<ElButton link type="primary" @click="handleViewDetail(scope.row)">查看详情</ElButton>
|
||||
<ElButton link type="primary" @click="handleViewFlow(scope.row)">流水记录</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 分页 -->
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="justify-content: flex-end; margin-top: 20px"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</ElCard>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="客户账号详情" width="800px" align-center>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem label="客户账号">{{ currentRow?.accountNo }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户名称">{{ currentRow?.customerName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户类型">
|
||||
<ElTag :type="currentRow?.customerType === 'agent' ? 'success' : 'primary'">
|
||||
{{ currentRow?.customerType === 'agent' ? '代理商' : '企业客户' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="联系电话">{{ currentRow?.phone }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金总额"
|
||||
>¥{{ currentRow?.totalCommission.toFixed(2) }}</ElDescriptionsItem
|
||||
>
|
||||
<ElDescriptionsItem label="可提现金额">
|
||||
<span style="color: var(--el-color-success)">
|
||||
¥{{ currentRow?.availableAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="待入账金额">
|
||||
<span style="color: var(--el-color-warning)">
|
||||
¥{{ currentRow?.pendingAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="已提现金额"
|
||||
>¥{{ currentRow?.withdrawnAmount.toFixed(2) }}</ElDescriptionsItem
|
||||
>
|
||||
<ElDescriptionsItem label="提现次数">{{ currentRow?.withdrawCount }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="最后提现时间">{{
|
||||
currentRow?.lastWithdrawTime
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="注册时间">{{ currentRow?.createTime }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注" :span="2">{{
|
||||
currentRow?.remark || '无'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CustomerAccount' })
|
||||
|
||||
const searchForm = reactive({
|
||||
accountNo: '',
|
||||
customerName: '',
|
||||
customerType: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 100
|
||||
})
|
||||
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
// 模拟表格数据
|
||||
const tableData = ref([
|
||||
{
|
||||
id: '1',
|
||||
accountNo: 'ACC20260001',
|
||||
customerName: '深圳市科技有限公司',
|
||||
customerType: 'agent',
|
||||
phone: '13800138000',
|
||||
totalCommission: 158900.5,
|
||||
availableAmount: 58900.5,
|
||||
pendingAmount: 50000.0,
|
||||
withdrawnAmount: 50000.0,
|
||||
withdrawCount: 12,
|
||||
lastWithdrawTime: '2026-01-08 15:00:00',
|
||||
createTime: '2025-06-01 10:00:00',
|
||||
remark: '优质代理商'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
accountNo: 'ACC20260002',
|
||||
customerName: '广州智能设备公司',
|
||||
customerType: 'enterprise',
|
||||
phone: '13900139000',
|
||||
totalCommission: 89600.0,
|
||||
availableAmount: 35600.0,
|
||||
pendingAmount: 24000.0,
|
||||
withdrawnAmount: 30000.0,
|
||||
withdrawCount: 8,
|
||||
lastWithdrawTime: '2026-01-05 10:30:00',
|
||||
createTime: '2025-07-15 14:20:00',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
accountNo: 'ACC20260003',
|
||||
customerName: '北京物联网代理',
|
||||
customerType: 'agent',
|
||||
phone: '13700137000',
|
||||
totalCommission: 256700.0,
|
||||
availableAmount: 106700.0,
|
||||
pendingAmount: 80000.0,
|
||||
withdrawnAmount: 70000.0,
|
||||
withdrawCount: 15,
|
||||
lastWithdrawTime: '2026-01-09 09:15:00',
|
||||
createTime: '2025-05-10 09:00:00',
|
||||
remark: '金牌代理商'
|
||||
}
|
||||
])
|
||||
|
||||
const handleSearch = () => {
|
||||
ElMessage.success('查询成功')
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.accountNo = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.customerType = ''
|
||||
ElMessage.info('已重置')
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: any) => {
|
||||
currentRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleViewFlow = (row: any) => {
|
||||
ElMessage.info(`查看 ${row.customerName} 的流水记录`)
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
ElMessage.info(`每页显示 ${size} 条`)
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
ElMessage.info(`当前第 ${page} 页`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,333 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 统计卡片 -->
|
||||
<ElRow :gutter="20" style="margin-bottom: 20px">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="stat-icon"
|
||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||
>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">佣金总额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.totalCommission.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="stat-icon"
|
||||
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
|
||||
>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">可提现金额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.availableAmount.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="stat-icon"
|
||||
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
|
||||
>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">待入账金额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.pendingAmount.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElCard shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="stat-icon"
|
||||
style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)"
|
||||
>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已提现金额</div>
|
||||
<div class="stat-value">¥{{ accountInfo.withdrawnAmount.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<ElRow style="margin-bottom: 20px">
|
||||
<ElButton type="primary" @click="showWithdrawDialog">申请提现</ElButton>
|
||||
<ElButton @click="viewWithdrawHistory">提现记录</ElButton>
|
||||
</ElRow>
|
||||
|
||||
<!-- 收支流水 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-weight: 500">收支流水</span>
|
||||
<ElRadioGroup v-model="flowType" size="small">
|
||||
<ElRadioButton value="all">全部</ElRadioButton>
|
||||
<ElRadioButton value="income">收入</ElRadioButton>
|
||||
<ElRadioButton value="withdraw">提现</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="filteredFlowData" index max-height="500">
|
||||
<template #default>
|
||||
<ElTableColumn label="流水号" prop="flowNo" min-width="180" />
|
||||
<ElTableColumn label="类型" prop="type">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.type === 'income' ? 'success' : 'warning'">
|
||||
{{ scope.row.type === 'income' ? '收入' : '提现' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="金额" prop="amount">
|
||||
<template #default="scope">
|
||||
<span
|
||||
:style="{
|
||||
color:
|
||||
scope.row.type === 'income'
|
||||
? 'var(--el-color-success)'
|
||||
: 'var(--el-color-danger)'
|
||||
}"
|
||||
>
|
||||
{{ scope.row.type === 'income' ? '+' : '-' }}¥{{ scope.row.amount.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="余额" prop="balance">
|
||||
<template #default="scope"> ¥{{ scope.row.balance.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="说明" prop="description" show-overflow-tooltip />
|
||||
<ElTableColumn label="时间" prop="createTime" width="180" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 提现申请对话框 -->
|
||||
<ElDialog v-model="withdrawDialogVisible" title="申请提现" width="600px" align-center>
|
||||
<ElForm
|
||||
ref="withdrawFormRef"
|
||||
:model="withdrawForm"
|
||||
:rules="withdrawRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="可提现金额">
|
||||
<span style="font-size: 20px; font-weight: 500; color: var(--el-color-success)">
|
||||
¥{{ accountInfo.availableAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="提现金额" prop="amount">
|
||||
<ElInputNumber
|
||||
v-model="withdrawForm.amount"
|
||||
:min="1"
|
||||
:max="accountInfo.availableAmount"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手续费">
|
||||
<span>¥{{ calculatedFee.toFixed(2) }}</span>
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
(费率: {{ feeRate * 100 }}%)
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实际到账">
|
||||
<span style="font-size: 18px; font-weight: 500; color: var(--el-color-success)">
|
||||
¥{{ actualAmount.toFixed(2) }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="收款银行" prop="bankName">
|
||||
<ElSelect
|
||||
v-model="withdrawForm.bankName"
|
||||
placeholder="请选择收款银行"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="中国工商银行" value="工商银行" />
|
||||
<ElOption label="中国建设银行" value="建设银行" />
|
||||
<ElOption label="中国农业银行" value="农业银行" />
|
||||
<ElOption label="中国银行" value="中国银行" />
|
||||
<ElOption label="招商银行" value="招商银行" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="银行账户" prop="bankAccount">
|
||||
<ElInput v-model="withdrawForm.bankAccount" placeholder="请输入银行账户" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开户姓名" prop="accountName">
|
||||
<ElInput v-model="withdrawForm.accountName" placeholder="请输入开户姓名" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="withdrawDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="submitWithdraw">提交申请</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'MyAccount' })
|
||||
|
||||
const accountInfo = reactive({
|
||||
totalCommission: 158900.5,
|
||||
availableAmount: 58900.5,
|
||||
pendingAmount: 50000.0,
|
||||
withdrawnAmount: 50000.0
|
||||
})
|
||||
|
||||
const flowType = ref('all')
|
||||
const withdrawDialogVisible = ref(false)
|
||||
const withdrawFormRef = ref<FormInstance>()
|
||||
const feeRate = 0.002 // 手续费率 0.2%
|
||||
|
||||
const withdrawForm = reactive({
|
||||
amount: 0,
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
accountName: ''
|
||||
})
|
||||
|
||||
const withdrawRules = reactive<FormRules>({
|
||||
amount: [
|
||||
{ required: true, message: '请输入提现金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value > accountInfo.availableAmount) {
|
||||
callback(new Error('提现金额不能大于可提现金额'))
|
||||
} else if (value < 1) {
|
||||
callback(new Error('提现金额不能小于1元'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
bankName: [{ required: true, message: '请选择收款银行', trigger: 'change' }],
|
||||
bankAccount: [{ required: true, message: '请输入银行账户', trigger: 'blur' }],
|
||||
accountName: [{ required: true, message: '请输入开户姓名', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const flowData = ref([
|
||||
{
|
||||
id: '1',
|
||||
flowNo: 'FL202601090001',
|
||||
type: 'income',
|
||||
amount: 1580.0,
|
||||
balance: 58900.5,
|
||||
description: '套餐销售佣金',
|
||||
createTime: '2026-01-09 10:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
flowNo: 'FL202601080001',
|
||||
type: 'withdraw',
|
||||
amount: 5000.0,
|
||||
balance: 57320.5,
|
||||
description: '提现到账',
|
||||
createTime: '2026-01-08 15:00:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
flowNo: 'FL202601070001',
|
||||
type: 'income',
|
||||
amount: 2360.0,
|
||||
balance: 62320.5,
|
||||
description: '号卡分配佣金',
|
||||
createTime: '2026-01-07 14:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
const calculatedFee = computed(() => {
|
||||
return withdrawForm.amount * feeRate
|
||||
})
|
||||
|
||||
const actualAmount = computed(() => {
|
||||
return withdrawForm.amount - calculatedFee.value
|
||||
})
|
||||
|
||||
const filteredFlowData = computed(() => {
|
||||
if (flowType.value === 'all') return flowData.value
|
||||
return flowData.value.filter((item) => item.type === flowType.value)
|
||||
})
|
||||
|
||||
const showWithdrawDialog = () => {
|
||||
withdrawForm.amount = 0
|
||||
withdrawForm.bankName = ''
|
||||
withdrawForm.bankAccount = ''
|
||||
withdrawForm.accountName = ''
|
||||
withdrawDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitWithdraw = async () => {
|
||||
if (!withdrawFormRef.value) return
|
||||
await withdrawFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('提现申请提交成功,请等待审核')
|
||||
withdrawDialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewWithdrawHistory = () => {
|
||||
ElMessage.info('查看提现记录')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-card {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 28px;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,9 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showCreateDialog">创建退款申请</ElButton>
|
||||
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('refund:create')"
|
||||
>创建退款申请</ElButton
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -32,34 +34,21 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 退款操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="refundOperationMenuRef"
|
||||
:menu-items="refundOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleRefundOperationMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 创建退款申请对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
title="创建退款申请"
|
||||
width="500px"
|
||||
width="30%"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
@@ -321,17 +310,15 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
defineOptions({ name: 'RefundList' })
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
@@ -348,10 +335,6 @@
|
||||
const resubmitDialogVisible = ref(false)
|
||||
const currentRefund = ref<Refund | null>(null)
|
||||
|
||||
// 右键菜单
|
||||
const refundOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingRefund = ref<Refund | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: RefundQueryParams = {
|
||||
status: undefined,
|
||||
@@ -971,88 +954,52 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 退款操作菜单项配置
|
||||
const refundOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
if (!currentOperatingRefund.value) return items
|
||||
// 获取操作按钮
|
||||
const getActions = (row: Refund) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 待审批状态可以审批通过、拒绝或退回
|
||||
if (currentOperatingRefund.value.status === 1) {
|
||||
items.push({
|
||||
key: 'approve',
|
||||
label: '审批通过'
|
||||
})
|
||||
items.push({
|
||||
key: 'reject',
|
||||
label: '审批拒绝'
|
||||
})
|
||||
items.push({
|
||||
key: 'return',
|
||||
label: '退回'
|
||||
})
|
||||
if (row.status === 1) {
|
||||
// 退回按钮直接显示
|
||||
if (hasAuth('refund:return')) {
|
||||
actions.push({
|
||||
label: '退回',
|
||||
handler: () => handleShowReturn(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
// 审批通过和审批拒绝放到更多里
|
||||
if (hasAuth('refund:approve')) {
|
||||
actions.push({
|
||||
label: '审批通过',
|
||||
handler: () => handleShowApprove(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth('refund:reject')) {
|
||||
actions.push({
|
||||
label: '审批拒绝',
|
||||
handler: () => handleShowReject(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 已拒绝或已退回状态可以重新提交
|
||||
if (currentOperatingRefund.value.status === 3 || currentOperatingRefund.value.status === 4) {
|
||||
items.push({
|
||||
key: 'resubmit',
|
||||
label: '重新提交'
|
||||
if (row.status === 3 || row.status === 4) {
|
||||
actions.push({
|
||||
label: '重新提交',
|
||||
handler: () => handleShowResubmit(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示退款操作右键菜单
|
||||
const showRefundOperationMenu = (e: MouseEvent, row: Refund) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingRefund.value = row
|
||||
refundOperationMenuRef.value?.show(e)
|
||||
return actions
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: Refund, column: any, event: MouseEvent) => {
|
||||
showRefundOperationMenu(event, row)
|
||||
}
|
||||
|
||||
// 处理退款操作菜单选择
|
||||
const handleRefundOperationMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentOperatingRefund.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'approve':
|
||||
handleShowApprove(currentOperatingRefund.value)
|
||||
break
|
||||
case 'reject':
|
||||
handleShowReject(currentOperatingRefund.value)
|
||||
break
|
||||
case 'return':
|
||||
handleShowReturn(currentOperatingRefund.value)
|
||||
break
|
||||
case 'resubmit':
|
||||
handleShowResubmit(currentOperatingRefund.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refund-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showCreateDialog" v-permission="'orders:add'">{{
|
||||
t('orderManagement.createOrder')
|
||||
'创建订单'
|
||||
}}</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
@@ -34,50 +34,37 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 创建订单对话框 -->
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
:title="t('orderManagement.createOrder')"
|
||||
:title="'创建订单'"
|
||||
width="600px"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<ElFormItem :label="t('orderManagement.table.orderType')" prop="order_type">
|
||||
<ElFormItem :label="'订单类型'" prop="order_type">
|
||||
<ElSelect
|
||||
v-model="createForm.order_type"
|
||||
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
|
||||
:placeholder="'请选择订单类型'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption :label="t('orderManagement.orderType.singleCard')" value="single_card" />
|
||||
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
|
||||
<ElOption :label="'单卡订单'" value="single_card" />
|
||||
<ElOption :label="'设备订单'" value="device" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.order_type === 'single_card'"
|
||||
:label="t('orderManagement.createForm.iotCardId')"
|
||||
:label="'IoT卡'"
|
||||
prop="iot_card_id"
|
||||
>
|
||||
<ElSelect
|
||||
@@ -85,7 +72,7 @@
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:placeholder="t('orderManagement.createForm.iotCardIdPlaceholder')"
|
||||
:placeholder="'请选择IoT卡'"
|
||||
:remote-method="searchIotCards"
|
||||
:loading="cardSearchLoading"
|
||||
style="width: 100%"
|
||||
@@ -106,17 +93,13 @@
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="createForm.order_type === 'device'"
|
||||
:label="t('orderManagement.createForm.deviceId')"
|
||||
prop="device_id"
|
||||
>
|
||||
<ElFormItem v-if="createForm.order_type === 'device'" :label="'设备'" prop="device_id">
|
||||
<ElSelect
|
||||
v-model="createForm.device_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:placeholder="t('orderManagement.createForm.deviceIdPlaceholder')"
|
||||
:placeholder="'请选择设备'"
|
||||
:remote-method="searchDevices"
|
||||
:loading="deviceSearchLoading"
|
||||
style="width: 100%"
|
||||
@@ -137,10 +120,10 @@
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('orderManagement.createForm.packageIds')" prop="package_ids">
|
||||
<ElFormItem :label="'套餐'" prop="package_ids">
|
||||
<ElSelect
|
||||
v-model="createForm.package_ids"
|
||||
:placeholder="t('orderManagement.createForm.packageIdsPlaceholder')"
|
||||
:placeholder="'请选择套餐'"
|
||||
multiple
|
||||
filterable
|
||||
remote
|
||||
@@ -158,13 +141,13 @@
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (¥${(pkg.cost_price / 100).toFixed(2)})`"
|
||||
:label="`${pkg.package_name} (¥${((pkg.cost_price || 0) / 100).toFixed(2)})`"
|
||||
:value="pkg.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ pkg.package_name }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
¥{{ (pkg.cost_price / 100).toFixed(2) }}
|
||||
¥{{ ((pkg.cost_price || 0) / 100).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
@@ -196,58 +179,52 @@
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="createDialogVisible = false">{{
|
||||
t('orderManagement.actions.cancel')
|
||||
}}</ElButton>
|
||||
<ElButton @click="createDialogVisible = false">{{ '取消' }}</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"
|
||||
>
|
||||
<ElDialog v-model="detailDialogVisible" :title="'订单详情'" width="800px">
|
||||
<div v-if="currentOrder" class="order-detail">
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.orderNo')">
|
||||
<ElDescriptionsItem :label="'订单编号'">
|
||||
{{ currentOrder.order_no }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.orderType')">
|
||||
<ElDescriptionsItem :label="'订单类型'">
|
||||
{{ getOrderTypeText(currentOrder.order_type) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.paymentStatus')">
|
||||
<ElDescriptionsItem :label="'支付状态'">
|
||||
<ElTag :type="getPaymentStatusType(currentOrder.payment_status)">
|
||||
{{ currentOrder.payment_status_text }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.totalAmount')">
|
||||
<ElDescriptionsItem :label="'订单金额'">
|
||||
{{ formatCurrency(currentOrder.total_amount) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.paymentMethod')">
|
||||
<ElDescriptionsItem :label="'支付方式'">
|
||||
{{ getPaymentMethodText(currentOrder.payment_method) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.buyerType')">
|
||||
<ElDescriptionsItem :label="'买家类型'">
|
||||
{{ getBuyerTypeText(currentOrder.buyer_type) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.commissionStatus')">
|
||||
<ElDescriptionsItem :label="'佣金状态'">
|
||||
{{ getCommissionStatusText(currentOrder.commission_status) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.paidAt')">
|
||||
<ElDescriptionsItem :label="'支付时间'">
|
||||
{{ currentOrder.paid_at ? formatDateTime(currentOrder.paid_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.createdAt')">
|
||||
<ElDescriptionsItem :label="'创建时间'">
|
||||
{{ formatDateTime(currentOrder.created_at) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="t('orderManagement.table.updatedAt')">
|
||||
<ElDescriptionsItem :label="'更新时间'">
|
||||
{{ formatDateTime(currentOrder.updated_at) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
@@ -257,28 +234,16 @@
|
||||
v-if="currentOrder.items && currentOrder.items.length > 0"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<h4>{{ t('orderManagement.orderItems') }}</h4>
|
||||
<h4>{{ '订单项' }}</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"
|
||||
>
|
||||
<ElTableColumn prop="package_name" :label="'套餐名称'" min-width="150" />
|
||||
<ElTableColumn prop="quantity" :label="'数量'" width="100" />
|
||||
<ElTableColumn prop="unit_price" :label="'单价'" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(row.unit_price) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="amount" :label="t('orderManagement.items.amount')" width="120">
|
||||
<ElTableColumn prop="amount" :label="'金额'" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(row.amount) }}
|
||||
</template>
|
||||
@@ -288,9 +253,7 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="detailDialogVisible = false">{{
|
||||
t('orderManagement.actions.close')
|
||||
}}</ElButton>
|
||||
<ElButton @click="detailDialogVisible = false">{{ '关闭' }}</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
@@ -301,7 +264,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { OrderService, CardService, DeviceService, PackageManageService } from '@/api/modules'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
@@ -318,45 +280,27 @@
|
||||
StandaloneIotCard,
|
||||
Device,
|
||||
PackageResponse,
|
||||
PurchaseCheckRequest,
|
||||
PurchaseCheckResponse
|
||||
PurchaseCheckRequest
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'OrderList' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
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 contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<Order | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState: OrderQueryParams = {
|
||||
@@ -385,37 +329,28 @@
|
||||
// 搜索表单配置
|
||||
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'),
|
||||
label: '支付状态',
|
||||
prop: 'payment_status',
|
||||
type: 'select',
|
||||
placeholder: t('orderManagement.searchForm.paymentStatusPlaceholder'),
|
||||
placeholder: '请选择支付状态',
|
||||
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 }
|
||||
{ label: '待支付', value: 1 },
|
||||
{ label: '已支付', value: 2 },
|
||||
{ label: '已取消', value: 3 },
|
||||
{ label: '已退款', value: 4 }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('orderManagement.searchForm.orderType'),
|
||||
label: '订单类型',
|
||||
prop: 'order_type',
|
||||
type: 'select',
|
||||
placeholder: t('orderManagement.searchForm.orderTypePlaceholder'),
|
||||
placeholder: '请选择订单类型',
|
||||
options: [
|
||||
{ label: t('orderManagement.orderType.singleCard'), value: 'single_card' },
|
||||
{ label: t('orderManagement.orderType.device'), value: 'device' }
|
||||
{ label: '单卡订单', value: 'single_card' },
|
||||
{ label: '设备订单', value: 'device' }
|
||||
],
|
||||
config: {
|
||||
clearable: true
|
||||
@@ -437,13 +372,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('orderManagement.searchForm.dateRange'),
|
||||
label: '创建时间',
|
||||
prop: 'dateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
clearable: true,
|
||||
startPlaceholder: t('orderManagement.searchForm.startDate'),
|
||||
endPlaceholder: t('orderManagement.searchForm.endDate'),
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss'
|
||||
}
|
||||
}
|
||||
@@ -458,19 +393,18 @@
|
||||
|
||||
// 列配置
|
||||
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: '订单编号', prop: 'order_no' },
|
||||
{ label: '订单类型', prop: 'order_type' },
|
||||
{ label: '买家类型', prop: 'buyer_type' },
|
||||
{ label: '订单渠道', prop: 'purchase_role' },
|
||||
{ label: '购买备注', prop: 'purchase_remark' },
|
||||
{ label: '操作者', prop: 'operator_name' },
|
||||
{ label: t('orderManagement.table.paymentStatus'), prop: 'payment_status' },
|
||||
{ label: t('orderManagement.table.totalAmount'), prop: 'total_amount' },
|
||||
{ label: '支付状态', prop: 'payment_status' },
|
||||
{ label: '订单金额', prop: 'total_amount' },
|
||||
{ label: '实付金额', prop: 'actual_paid_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: '支付方式', prop: 'payment_method' },
|
||||
{ label: '支付时间', prop: 'paid_at' },
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
const createFormRef = ref<FormInstance>()
|
||||
@@ -479,14 +413,14 @@
|
||||
order_type: [
|
||||
{
|
||||
required: true,
|
||||
message: t('orderManagement.validation.orderTypeRequired'),
|
||||
message: '请选择订单类型',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
package_ids: [
|
||||
{
|
||||
required: true,
|
||||
message: t('orderManagement.validation.packageIdsRequired'),
|
||||
message: '请选择套餐',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
@@ -623,25 +557,21 @@
|
||||
|
||||
// 获取订单类型文本
|
||||
const getOrderTypeText = (type: OrderType): string => {
|
||||
return type === 'single_card'
|
||||
? t('orderManagement.orderType.singleCard')
|
||||
: t('orderManagement.orderType.device')
|
||||
return type === 'single_card' ? '单卡订单' : '设备订单'
|
||||
}
|
||||
|
||||
// 获取买家类型文本
|
||||
const getBuyerTypeText = (type: BuyerType): string => {
|
||||
return type === 'personal'
|
||||
? t('orderManagement.buyerType.personal')
|
||||
: t('orderManagement.buyerType.agent')
|
||||
return type === 'personal' ? '个人' : '代理'
|
||||
}
|
||||
|
||||
// 获取支付方式文本
|
||||
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'),
|
||||
offline: t('orderManagement.paymentMethod.offline')
|
||||
wallet: '钱包',
|
||||
wechat: '微信',
|
||||
alipay: '支付宝',
|
||||
offline: '线下'
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
@@ -649,23 +579,18 @@
|
||||
// 获取佣金状态文本
|
||||
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')
|
||||
0: '不适用',
|
||||
1: '待结算',
|
||||
2: '已结算'
|
||||
}
|
||||
return statusMap[status] || '-'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'id',
|
||||
label: t('orderManagement.table.id'),
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'order_no',
|
||||
label: t('orderManagement.table.orderNo'),
|
||||
label: '订单编号',
|
||||
minWidth: 220,
|
||||
formatter: (row: Order) => {
|
||||
return h(
|
||||
@@ -683,7 +608,7 @@
|
||||
},
|
||||
{
|
||||
prop: 'order_type',
|
||||
label: t('orderManagement.table.orderType'),
|
||||
label: '订单类型',
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(ElTag, { type: row.order_type === 'single_card' ? 'primary' : 'success' }, () =>
|
||||
@@ -693,7 +618,7 @@
|
||||
},
|
||||
{
|
||||
prop: 'buyer_type',
|
||||
label: t('orderManagement.table.buyerType'),
|
||||
label: '买家类型',
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(ElTag, { type: row.buyer_type === 'personal' ? 'info' : 'warning' }, () =>
|
||||
@@ -713,8 +638,8 @@
|
||||
purchased_by_platform: 'danger',
|
||||
purchase_for_subordinate: 'info'
|
||||
}
|
||||
return h(ElTag, { type: roleTypeMap[row.purchase_role] || 'info', size: 'small' }, () =>
|
||||
getPurchaseRoleText(row.purchase_role)
|
||||
return h(ElTag, { type: roleTypeMap[row.purchase_role || ''] || 'info', size: 'small' }, () =>
|
||||
getPurchaseRoleText(row.purchase_role || '')
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -733,7 +658,7 @@
|
||||
},
|
||||
{
|
||||
prop: 'payment_status',
|
||||
label: t('orderManagement.table.paymentStatus'),
|
||||
label: '支付状态',
|
||||
width: 120,
|
||||
formatter: (row: Order) => {
|
||||
return h(
|
||||
@@ -745,7 +670,7 @@
|
||||
},
|
||||
{
|
||||
prop: 'total_amount',
|
||||
label: t('orderManagement.table.totalAmount'),
|
||||
label: '订单金额',
|
||||
width: 120,
|
||||
formatter: (row: Order) => formatCurrency(row.total_amount)
|
||||
},
|
||||
@@ -760,19 +685,19 @@
|
||||
},
|
||||
{
|
||||
prop: 'payment_method',
|
||||
label: t('orderManagement.table.paymentMethod'),
|
||||
label: '支付方式',
|
||||
width: 120,
|
||||
formatter: (row: Order) => getPaymentMethodText(row.payment_method)
|
||||
},
|
||||
{
|
||||
prop: 'paid_at',
|
||||
label: t('orderManagement.table.paidAt'),
|
||||
label: '支付时间',
|
||||
width: 180,
|
||||
formatter: (row: Order) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: t('orderManagement.table.createdAt'),
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: Order) => formatDateTime(row.created_at)
|
||||
}
|
||||
@@ -1006,8 +931,11 @@
|
||||
|
||||
// 调用套餐购买预检接口
|
||||
const checkData: PurchaseCheckRequest = {
|
||||
order_type: createForm.order_type,
|
||||
resource_id: createForm.order_type === 'single_card' ? createForm.iot_card_id! : createForm.device_id!,
|
||||
order_type: createForm.order_type!,
|
||||
resource_id:
|
||||
createForm.order_type === 'single_card'
|
||||
? createForm.iot_card_id!
|
||||
: createForm.device_id!,
|
||||
package_ids: createForm.package_ids
|
||||
}
|
||||
|
||||
@@ -1042,7 +970,7 @@
|
||||
if (createForm.payment_method === 'wallet') {
|
||||
ElMessage.success('订单创建成功,已自动完成支付')
|
||||
} else {
|
||||
ElMessage.success(t('orderManagement.messages.createSuccess'))
|
||||
ElMessage.success('订单创建成功')
|
||||
}
|
||||
|
||||
createDialogVisible.value = false
|
||||
@@ -1081,23 +1009,19 @@
|
||||
const handleCancelOrder = (row: Order) => {
|
||||
// 已支付的订单不能取消
|
||||
if (row.payment_status === 2) {
|
||||
ElMessage.warning(t('orderManagement.messages.cannotCancelPaid'))
|
||||
ElMessage.warning('已支付的订单无法取消')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
t('orderManagement.messages.cancelConfirmText'),
|
||||
t('orderManagement.messages.cancelConfirm'),
|
||||
{
|
||||
confirmButtonText: t('orderManagement.actions.confirm'),
|
||||
cancelButtonText: t('orderManagement.actions.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
ElMessageBox.confirm('确定要取消该订单吗?', '取消订单', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
await OrderService.cancelOrder(row.id)
|
||||
ElMessage.success(t('orderManagement.messages.cancelSuccess'))
|
||||
ElMessage.success('订单已取消')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -1108,40 +1032,20 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 右键菜单项配置
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
if (!currentRow.value) return []
|
||||
|
||||
const items: MenuItemType[] = []
|
||||
// 获取操作按钮
|
||||
const getActions = (row: Order) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 只有待支付和已支付的订单可以删除
|
||||
if (
|
||||
(currentRow.value.payment_status === 1 || currentRow.value.payment_status === 2) &&
|
||||
hasAuth('orders:delete')
|
||||
) {
|
||||
items.push({ key: 'cancel', label: '删除' })
|
||||
if ((row.payment_status === 1 || row.payment_status === 2) && hasAuth('orders:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleCancelOrder(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: Order, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'cancel':
|
||||
handleCancelOrder(currentRow.value)
|
||||
break
|
||||
}
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-batch-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">查询</ElButton>
|
||||
<ElButton type="success" @click="showImportDialog">导入</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="importDialogVisible"
|
||||
title="批量导入套餐"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 顶部下载模板按钮 -->
|
||||
<div class="template-section">
|
||||
<ElButton type="primary" @click="downloadTemplate" icon="Download"> 下载模板 </ElButton>
|
||||
<span class="template-tip">请先下载模板,按模板格式填写后上传</span>
|
||||
</div>
|
||||
|
||||
<ElDivider />
|
||||
|
||||
<ElForm
|
||||
ref="importFormRef"
|
||||
:model="importFormData"
|
||||
:rules="importRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="导入类型" prop="importType">
|
||||
<ElSelect
|
||||
v-model="importFormData.importType"
|
||||
placeholder="请选择导入类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="套餐批量导入" value="package_batch" />
|
||||
<ElOption label="套餐更新导入" value="package_update" />
|
||||
<ElOption label="套餐停用导入" value="package_disable" />
|
||||
<ElOption label="套餐启用导入" value="package_enable" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="上传Excel文件" prop="excelFile">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:auto-upload="false"
|
||||
accept=".xlsx,.xls"
|
||||
drag
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text"> 将文件拖到此处,或<em>点击上传</em> </div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip"> 只能上传 xlsx/xls 文件,且不超过 10MB </div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="importFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="importDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleImportSubmit" :loading="importLoading">
|
||||
确认导入
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElUpload, ElIcon, ElDivider } from 'element-plus'
|
||||
import { UploadFilled, Download } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, UploadInstance, UploadRawFile, UploadFile } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageBatch' })
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const importLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
importType: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 导入表单实例
|
||||
const importFormRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 导入表单数据
|
||||
const importFormData = reactive({
|
||||
importType: '',
|
||||
excelFile: null as File | null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
importTotal: 1000,
|
||||
success: 980,
|
||||
failed: 20,
|
||||
importType: '套餐批量导入',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-08 10:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
importTotal: 500,
|
||||
success: 500,
|
||||
failed: 0,
|
||||
importType: '套餐更新导入',
|
||||
operator: '孔丽娟',
|
||||
operationTime: '2025-11-07 14:15:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
importTotal: 300,
|
||||
success: 285,
|
||||
failed: 15,
|
||||
importType: '套餐停用导入',
|
||||
operator: '李佳音',
|
||||
operationTime: '2025-11-06 09:45:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
importTotal: 800,
|
||||
success: 795,
|
||||
failed: 5,
|
||||
importType: '套餐启用导入',
|
||||
operator: '赵强',
|
||||
operationTime: '2025-11-05 16:20:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
importTotal: 150,
|
||||
success: 145,
|
||||
failed: 5,
|
||||
importType: '套餐批量导入',
|
||||
operator: '张若暄',
|
||||
operationTime: '2025-11-04 11:30:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '导入类型',
|
||||
prop: 'importType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '套餐批量导入', value: 'package_batch' },
|
||||
{ label: '套餐更新导入', value: 'package_update' },
|
||||
{ label: '套餐停用导入', value: 'package_disable' },
|
||||
{ label: '套餐启用导入', value: 'package_enable' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '导入总数', prop: 'importTotal' },
|
||||
{ label: '成功', prop: 'success' },
|
||||
{ label: '失败', prop: 'failed' },
|
||||
{ label: '导入类型', prop: 'importType' },
|
||||
{ label: '操作人', prop: 'operator' },
|
||||
{ label: '操作时间', prop: 'operationTime' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取导入类型标签类型
|
||||
const getImportTypeTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case '套餐批量导入':
|
||||
return 'primary'
|
||||
case '套餐更新导入':
|
||||
return 'success'
|
||||
case '套餐停用导入':
|
||||
return 'danger'
|
||||
case '套餐启用导入':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入对话框
|
||||
const showImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (importFormRef.value) {
|
||||
importFormRef.value.resetFields()
|
||||
}
|
||||
importFormData.importType = ''
|
||||
importFormData.excelFile = null
|
||||
importFormData.remark = ''
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
ElMessage.success('正在下载批量导入模板...')
|
||||
// 这里可以实现实际的模板下载功能
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看导入详情: ${row.importType} - 导入总数:${row.importTotal}`)
|
||||
}
|
||||
|
||||
// 重新导入
|
||||
const retryImport = (row: any) => {
|
||||
ElMessage.info(`重新导入: ${row.importType}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessage.info(`删除导入记录: ${row.id}`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'importTotal',
|
||||
label: '导入总数',
|
||||
formatter: (row) => `${row.importTotal} `
|
||||
},
|
||||
{
|
||||
prop: 'success',
|
||||
label: '成功',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: 'success' }, () => `${row.success}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'failed',
|
||||
label: '失败',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.failed > 0 ? 'danger' : 'success' }, () => `${row.failed}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'importType',
|
||||
label: '导入类型',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getImportTypeTagType(row.importType) }, () => row.importType)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '操作人'
|
||||
},
|
||||
{
|
||||
prop: 'operationTime',
|
||||
label: '操作时间'
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '重试',
|
||||
onClick: () => retryImport(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getBatchImportList()
|
||||
})
|
||||
|
||||
// 获取批量导入列表
|
||||
const getBatchImportList = 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
|
||||
} catch (error) {
|
||||
console.error('获取批量导入列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getBatchImportList()
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('最多只能上传一个文件')
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const beforeUpload = (file: UploadRawFile) => {
|
||||
const isExcel =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel'
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
|
||||
if (!isExcel) {
|
||||
ElMessage.error('只能上传 Excel 文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('上传文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (file: UploadFile) => {
|
||||
if (file.raw) {
|
||||
importFormData.excelFile = file.raw
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单验证规则
|
||||
const importRules = reactive<FormRules>({
|
||||
importType: [{ required: true, message: '请选择导入类型', trigger: 'change' }],
|
||||
excelFile: [{ required: true, message: '请上传Excel文件', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交导入
|
||||
const handleImportSubmit = async () => {
|
||||
if (!importFormRef.value) return
|
||||
|
||||
// 检查文件是否上传
|
||||
if (!importFormData.excelFile) {
|
||||
ElMessage.error('请先上传Excel文件')
|
||||
return
|
||||
}
|
||||
|
||||
await importFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
importLoading.value = true
|
||||
|
||||
// 模拟导入过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(`${importFormData.importType}批量导入提交成功!`)
|
||||
importDialogVisible.value = false
|
||||
importLoading.value = false
|
||||
getBatchImportList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-batch-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
.template-section {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.template-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,585 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-change-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">查询</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="changeDialogVisible"
|
||||
title="套餐变更"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm
|
||||
ref="changeFormRef"
|
||||
:model="changeFormData"
|
||||
:rules="changeRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="当前套餐" prop="currentPackage">
|
||||
<ElInput
|
||||
v-model="changeFormData.currentPackage"
|
||||
placeholder="当前套餐"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="变更套餐" prop="newPackage">
|
||||
<ElSelect
|
||||
v-model="changeFormData.newPackage"
|
||||
placeholder="请选择要变更的套餐"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="随意联畅玩年卡套餐" value="changwan_yearly" />
|
||||
<ElOption label="如意包年3G流量包" value="ruyi_3g" />
|
||||
<ElOption label="Y-NB专享套餐" value="nb_special" />
|
||||
<ElOption label="100G全国流量月卡套餐" value="big_data_100g" />
|
||||
<ElOption label="广电飞悦卡无预存50G" value="gdtv_50g" />
|
||||
<ElOption label="联通大王卡" value="unicom_dawang" />
|
||||
<ElOption label="移动花卡宝藏版" value="mobile_huaka" />
|
||||
<ElOption label="电信星卡" value="telecom_star" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="变更原因" prop="changeReason">
|
||||
<ElSelect
|
||||
v-model="changeFormData.changeReason"
|
||||
placeholder="请选择变更原因"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<ElOption label="用户主动变更" value="user_request" />
|
||||
<ElOption label="套餐到期升级" value="package_upgrade" />
|
||||
<ElOption label="业务需求调整" value="business_adjustment" />
|
||||
<ElOption label="系统自动变更" value="system_auto" />
|
||||
<ElOption label="其他原因" value="other" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注说明" prop="remark">
|
||||
<ElInput
|
||||
v-model="changeFormData.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注说明(可选)"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="changeDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleChangeSubmit" :loading="changeLoading">
|
||||
确认变更
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageChange' })
|
||||
|
||||
const changeDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const changeLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
isDistribution: '',
|
||||
cardStatus: '',
|
||||
cardType: '',
|
||||
cardCompany: '',
|
||||
distributor: '',
|
||||
iccid: '',
|
||||
accessNumber: '',
|
||||
virtualNumber: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 套餐变更表单实例
|
||||
const changeFormRef = ref<FormInstance>()
|
||||
|
||||
// 套餐变更表单数据
|
||||
const changeFormData = reactive({
|
||||
currentPackage: '',
|
||||
newPackage: '',
|
||||
changeReason: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRowData = ref<any>({})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
iccid: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
virtualNumber: '100001',
|
||||
cardCompany: '联通',
|
||||
expiryDate: '2025-12-31',
|
||||
packageName: '随意联畅玩年卡套餐',
|
||||
cardType: '2G/3G/4G',
|
||||
distributorName: '北京优享科技有限公司',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iccid: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
virtualNumber: '100002',
|
||||
cardCompany: '移动',
|
||||
expiryDate: '2025-11-30',
|
||||
packageName: '如意包年3G流量包',
|
||||
cardType: 'NB-IoT',
|
||||
distributorName: '上海智联通信技术公司',
|
||||
cardStatus: '停机'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
iccid: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
virtualNumber: '100003',
|
||||
cardCompany: '电信',
|
||||
expiryDate: '2026-01-15',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
cardType: '4G/5G',
|
||||
distributorName: '广州物联网络有限公司',
|
||||
cardStatus: '正常'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
iccid: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
virtualNumber: '100004',
|
||||
cardCompany: '联通',
|
||||
expiryDate: '2025-10-20',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
cardType: '2G/3G/4G',
|
||||
distributorName: '深圳云联科技股份公司',
|
||||
cardStatus: '欠费停机'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
iccid: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
virtualNumber: '100005',
|
||||
cardCompany: '广电',
|
||||
expiryDate: '2025-09-10',
|
||||
packageName: '广电飞悦卡无预存50G',
|
||||
cardType: '4G/5G',
|
||||
distributorName: '杭州通达网络服务公司',
|
||||
cardStatus: '正常'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '是否分销',
|
||||
prop: 'isDistribution',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: 'yes' },
|
||||
{ label: '否', value: 'no' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '卡状态',
|
||||
prop: 'cardStatus',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '正常', value: 'normal' },
|
||||
{ label: '停机', value: 'suspended' },
|
||||
{ label: '欠费停机', value: 'overdue_suspended' },
|
||||
{ label: '已销户', value: 'closed' },
|
||||
{ label: '测试期', value: 'testing' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '卡片类型',
|
||||
prop: 'cardType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '2G/3G/4G', value: '2g_3g_4g' },
|
||||
{ label: '4G/5G', value: '4g_5g' },
|
||||
{ label: 'NB-IoT', value: 'nb_iot' },
|
||||
{ label: 'eMTC', value: 'emtc' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '开卡公司',
|
||||
prop: 'cardCompany',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '联通', value: 'unicom' },
|
||||
{ label: '移动', value: 'mobile' },
|
||||
{ label: '电信', value: 'telecom' },
|
||||
{ label: '广电', value: 'gdtv' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '分销商',
|
||||
prop: 'distributor',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入分销商'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: 'ICCID号',
|
||||
prop: 'iccid',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入ICCID号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '接入号',
|
||||
prop: 'accessNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入接入号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号',
|
||||
prop: 'virtualNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: 'ICCID号', prop: 'iccid' },
|
||||
{ label: '接入号码', prop: 'accessNumber' },
|
||||
{ label: '虚拟号', prop: 'virtualNumber' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '到期时间', prop: 'expiryDate' },
|
||||
{ label: '套餐', prop: 'packageName' },
|
||||
{ label: '卡片类型', prop: 'cardType' },
|
||||
{ label: '分销商姓名', prop: 'distributorName' },
|
||||
{ label: '卡状态', prop: 'cardStatus' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 获取卡状态标签类型
|
||||
const getCardStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '正常':
|
||||
return 'success'
|
||||
case '停机':
|
||||
return 'warning'
|
||||
case '欠费停机':
|
||||
return 'danger'
|
||||
case '已销户':
|
||||
return 'info'
|
||||
case '测试期':
|
||||
return 'primary'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐变更记录`)
|
||||
}
|
||||
|
||||
// 显示套餐变更对话框
|
||||
const showChangeDialog = (row: any) => {
|
||||
changeDialogVisible.value = true
|
||||
selectedRowData.value = row
|
||||
// 重置表单
|
||||
if (changeFormRef.value) {
|
||||
changeFormRef.value.resetFields()
|
||||
}
|
||||
changeFormData.currentPackage = row.packageName
|
||||
changeFormData.newPackage = ''
|
||||
changeFormData.changeReason = ''
|
||||
changeFormData.remark = ''
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'iccid',
|
||||
label: 'ICCID号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号码',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'virtualNumber',
|
||||
label: '虚拟号',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'expiryDate',
|
||||
label: '到期时间',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'cardType',
|
||||
label: '卡片类型',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'distributorName',
|
||||
label: '分销商姓名',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'cardStatus',
|
||||
label: '卡状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getCardStatusType(row.cardStatus) }, () => row.cardStatus)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 120,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '套餐变更',
|
||||
onClick: () => showChangeDialog(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageChangeList()
|
||||
})
|
||||
|
||||
// 获取套餐变更列表
|
||||
const getPackageChangeList = 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
|
||||
} catch (error) {
|
||||
console.error('获取套餐变更列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageChangeList()
|
||||
}
|
||||
|
||||
// 套餐变更表单验证规则
|
||||
const changeRules = reactive<FormRules>({
|
||||
newPackage: [{ required: true, message: '请选择要变更的套餐', trigger: 'change' }],
|
||||
changeReason: [{ required: true, message: '请选择变更原因', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交套餐变更
|
||||
const handleChangeSubmit = async () => {
|
||||
if (!changeFormRef.value) return
|
||||
|
||||
await changeFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
changeLoading.value = true
|
||||
|
||||
// 模拟变更过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`套餐变更成功!ICCID:${selectedRowData.value.iccid},从${changeFormData.currentPackage}变更为${changeFormData.newPackage}`
|
||||
)
|
||||
changeDialogVisible.value = false
|
||||
changeLoading.value = false
|
||||
getPackageChangeList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-change-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,452 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-commission-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@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="handleSearch">搜索</ElButton>
|
||||
<ElButton type="success" @click="showAddDialog">新增</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@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="addDialogVisible"
|
||||
title="新增套餐佣金网卡"
|
||||
width="500px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="addFormRef" :model="addFormData" :rules="addRules" label-width="120px">
|
||||
<ElFormItem label="卡号" prop="cardNumber">
|
||||
<ElInput v-model="addFormData.cardNumber" placeholder="请输入卡号" clearable />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="月份" prop="month">
|
||||
<ElDatePicker
|
||||
v-model="addFormData.month"
|
||||
type="month"
|
||||
placeholder="请选择月份"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM"
|
||||
value-format="YYYY-MM"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="addDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAddSubmit" :loading="addLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElDatePicker } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageCommission' })
|
||||
|
||||
const addDialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const addLoading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
cardNumber: '',
|
||||
virtualNumber: '',
|
||||
createDateRange: []
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 新增表单实例
|
||||
const addFormRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const addFormData = reactive({
|
||||
cardNumber: '',
|
||||
month: ''
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
cardNumber: '89860621370079892035',
|
||||
accessNumber: '1440012345678',
|
||||
virtualNumber: '100001',
|
||||
cardCompany: '联通',
|
||||
month: '2025-11',
|
||||
createTime: '2025-11-08 10:30:00',
|
||||
creator: '张若暄'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cardNumber: '89860621370079892036',
|
||||
accessNumber: '1440012345679',
|
||||
virtualNumber: '100002',
|
||||
cardCompany: '移动',
|
||||
month: '2025-11',
|
||||
createTime: '2025-11-07 14:15:00',
|
||||
creator: '孔丽娟'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cardNumber: '89860621370079892037',
|
||||
accessNumber: '1440012345680',
|
||||
virtualNumber: '100003',
|
||||
cardCompany: '电信',
|
||||
month: '2025-11',
|
||||
createTime: '2025-11-06 09:45:00',
|
||||
creator: '李佳音'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cardNumber: '89860621370079892038',
|
||||
accessNumber: '1440012345681',
|
||||
virtualNumber: '100004',
|
||||
cardCompany: '联通',
|
||||
month: '2025-10',
|
||||
createTime: '2025-10-20 16:20:00',
|
||||
creator: '赵强'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cardNumber: '89860621370079892039',
|
||||
accessNumber: '1440012345682',
|
||||
virtualNumber: '100005',
|
||||
cardCompany: '广电',
|
||||
month: '2025-10',
|
||||
createTime: '2025-10-15 11:30:00',
|
||||
creator: '张若暄'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '卡号',
|
||||
prop: 'cardNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入卡号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '虚拟号',
|
||||
prop: 'virtualNumber',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入虚拟号'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createDateRange',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '卡号', prop: 'cardNumber' },
|
||||
{ label: '接入号', prop: 'accessNumber' },
|
||||
{ label: '虚拟号', prop: 'virtualNumber' },
|
||||
{ label: '开卡公司', prop: 'cardCompany' },
|
||||
{ label: '月份', prop: 'month' },
|
||||
{ label: '创建时间', prop: 'createTime' },
|
||||
{ label: '创建人', prop: 'creator' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐佣金网卡记录`)
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showAddDialog = () => {
|
||||
addDialogVisible.value = true
|
||||
// 重置表单
|
||||
if (addFormRef.value) {
|
||||
addFormRef.value.resetFields()
|
||||
}
|
||||
addFormData.cardNumber = ''
|
||||
addFormData.month = ''
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (row: any) => {
|
||||
ElMessage.info(`查看详情: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const editRecord = (row: any) => {
|
||||
ElMessage.info(`编辑记录: ${row.cardNumber}`)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除卡号为"${row.cardNumber}"的佣金记录吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
getCommissionCardList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'cardNumber',
|
||||
label: '卡号',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'accessNumber',
|
||||
label: '接入号',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
prop: 'virtualNumber',
|
||||
label: '虚拟号',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cardCompany',
|
||||
label: '开卡公司',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'month',
|
||||
label: '月份',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'creator',
|
||||
label: '创建人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 220,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '查看',
|
||||
onClick: () => viewDetails(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editRecord(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '删除',
|
||||
onClick: () => deleteRecord(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getCommissionCardList()
|
||||
})
|
||||
|
||||
// 获取套餐佣金网卡列表
|
||||
const getCommissionCardList = 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
|
||||
} catch (error) {
|
||||
console.error('获取套餐佣金网卡列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCommissionCardList()
|
||||
}
|
||||
|
||||
// 新增表单验证规则
|
||||
const addRules = reactive<FormRules>({
|
||||
cardNumber: [
|
||||
{ required: true, message: '请输入卡号', trigger: 'blur' },
|
||||
{ min: 15, max: 20, message: '卡号长度在 15 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
month: [{ required: true, message: '请选择月份', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交新增
|
||||
const handleAddSubmit = async () => {
|
||||
if (!addFormRef.value) return
|
||||
|
||||
await addFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
addLoading.value = true
|
||||
|
||||
// 模拟新增过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`新增套餐佣金网卡成功!卡号:${addFormData.cardNumber},月份:${addFormData.month}`
|
||||
)
|
||||
addDialogVisible.value = false
|
||||
addLoading.value = false
|
||||
getCommissionCardList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-commission-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,955 +0,0 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="package-create-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 数据视图组件 -->
|
||||
<ArtDataViewer
|
||||
ref="dataViewerRef"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:table-columns="columns"
|
||||
:descriptions-fields="descriptionsFields"
|
||||
:descriptions-columns="2"
|
||||
:pagination="pagination"
|
||||
:label-width="'140px'"
|
||||
:field-columns="columnChecks"
|
||||
:show-card-actions="true"
|
||||
:show-card-selection="true"
|
||||
:default-view="currentView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@view-change="handleViewChange"
|
||||
>
|
||||
<template #header-left>
|
||||
<ElButton type="success" @click="showCreateDialog">新增套餐</ElButton>
|
||||
<ElButton @click="exportExcel">导出excel</ElButton>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:currentView="currentView"
|
||||
@refresh="handleRefresh"
|
||||
@viewChange="handleViewChange"
|
||||
:show-title="false"
|
||||
:show-view-toggle="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-actions="{ item }">
|
||||
<ElButton type="primary" size="small" @click="editPackage(item)"> 编辑 </ElButton>
|
||||
<ElButton type="warning" size="small" @click="offlinePackage(item)"> 下架 </ElButton>
|
||||
<ElButton type="success" size="small" @click="copyPackage(item)"> 复制套餐 </ElButton>
|
||||
</template>
|
||||
</ArtDataViewer>
|
||||
|
||||
<!-- 新增套餐对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="新增套餐"
|
||||
width="55%"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="150px">
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐系列" prop="series">
|
||||
<ElSelect
|
||||
v-model="formData.series"
|
||||
placeholder="请选择套餐系列"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="畅玩系列" value="changwan" />
|
||||
<ElOption label="如意系列" value="ruyi" />
|
||||
<ElOption label="NB专享" value="nb_special" />
|
||||
<ElOption label="大流量系列" value="big_data" />
|
||||
<ElOption label="广电系列" value="gdtv" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="余额充值梯度" prop="balanceRechargeGradient">
|
||||
<ElSelect
|
||||
v-model="formData.balanceRechargeGradient"
|
||||
placeholder="请选择充值梯度"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="10,20,50,100" value="10,20,50,100" />
|
||||
<ElOption label="5,10,20,50" value="5,10,20,50" />
|
||||
<ElOption label="10,50,100" value="10,50,100" />
|
||||
<ElOption label="20,50,100,200" value="20,50,100,200" />
|
||||
<ElOption label="10,30,50" value="10,30,50" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐名称" prop="name">
|
||||
<ElInput v-model="formData.name" placeholder="请输入套餐名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐类型" prop="type">
|
||||
<ElSelect
|
||||
v-model="formData.type"
|
||||
placeholder="请选择套餐类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="月套餐" value="monthly" />
|
||||
<ElOption label="年套餐" value="yearly" />
|
||||
<ElOption label="流量包" value="data_package" />
|
||||
<ElOption label="季度套餐" value="quarterly" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="本月次月加餐包" prop="monthlyNextMonthAddon">
|
||||
<ElSelect
|
||||
v-model="formData.monthlyNextMonthAddon"
|
||||
placeholder="请选择加餐包支持"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption label="支持" value="support" />
|
||||
<ElOption label="不支持" value="not_support" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="本月最大充值数量" prop="maxMonthlyRecharge">
|
||||
<ElInputNumber
|
||||
v-model="formData.maxMonthlyRecharge"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
placeholder="请输入本月最大充值数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="次月最大充值数量" prop="maxNextMonthRecharge">
|
||||
<ElInputNumber
|
||||
v-model="formData.maxNextMonthRecharge"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
placeholder="请输入次月最大充值数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐数量" prop="packageCount">
|
||||
<ElInputNumber
|
||||
v-model="formData.packageCount"
|
||||
:min="1"
|
||||
:max="999999"
|
||||
placeholder="请输入套餐数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="成本价" prop="costPrice">
|
||||
<ElInputNumber
|
||||
v-model="formData.costPrice"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入成本价"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐金额" prop="packageAmount">
|
||||
<ElInputNumber
|
||||
v-model="formData.packageAmount"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="2"
|
||||
placeholder="请输入套餐金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认新增
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ElTag, ElMessage, ElMessageBox, ElInputNumber } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'PackageCreate' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 当前视图模式
|
||||
const currentView = ref('table')
|
||||
|
||||
// 数据视图组件引用
|
||||
const dataViewerRef = ref()
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
packageType: '',
|
||||
packageName: '',
|
||||
packageCode: '',
|
||||
status: '',
|
||||
packageSeries: '',
|
||||
levelOneMerge: '',
|
||||
levelTwoMerge: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 新增表单数据
|
||||
const formData = reactive({
|
||||
series: '',
|
||||
balanceRechargeGradient: '',
|
||||
name: '',
|
||||
type: '',
|
||||
monthlyNextMonthAddon: '',
|
||||
maxMonthlyRecharge: 0,
|
||||
maxNextMonthRecharge: 0,
|
||||
packageCount: 0,
|
||||
costPrice: 0,
|
||||
packageAmount: 0
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
packageSeries: '畅玩系列',
|
||||
packageName: '随意联畅玩年卡套餐',
|
||||
packageTraffic: 120,
|
||||
packageCount: 1000,
|
||||
packageCountType: '张',
|
||||
packageType: '年套餐',
|
||||
packageAmount: '168.00',
|
||||
description: '年卡套餐,含120GB流量',
|
||||
frontendSort: 1,
|
||||
sort: 1,
|
||||
maxMonthlyRecharge: 50,
|
||||
maxNextMonthRecharge: 50,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '是',
|
||||
levelOneMergeCode: 'L1_001',
|
||||
levelTwoMerge: '否',
|
||||
levelTwoMergeCode: '',
|
||||
isSpecial: '否',
|
||||
creator: '张若暄',
|
||||
modifier: '张若暄',
|
||||
addTime: '2025-11-08 10:30:00',
|
||||
balanceRechargeGradient: '10,20,50,100',
|
||||
packageCode: 'PKG_001'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
packageSeries: '如意系列',
|
||||
packageName: '如意包年3G流量包',
|
||||
packageTraffic: 36,
|
||||
packageCount: 800,
|
||||
packageCountType: '张',
|
||||
packageType: '年套餐',
|
||||
packageAmount: '98.00',
|
||||
description: '年包3GB月套餐',
|
||||
frontendSort: 2,
|
||||
sort: 2,
|
||||
maxMonthlyRecharge: 30,
|
||||
maxNextMonthRecharge: 30,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '是',
|
||||
levelOneMergeCode: 'L1_002',
|
||||
levelTwoMerge: '是',
|
||||
levelTwoMergeCode: 'L2_001',
|
||||
isSpecial: '否',
|
||||
creator: '孔丽娟',
|
||||
modifier: '孔丽娟',
|
||||
addTime: '2025-11-07 14:15:00',
|
||||
balanceRechargeGradient: '5,10,20,50',
|
||||
packageCode: 'PKG_002'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
packageSeries: 'NB专享',
|
||||
packageName: 'Y-NB专享套餐',
|
||||
packageTraffic: 24,
|
||||
packageCount: 500,
|
||||
packageCountType: '张',
|
||||
packageType: '月套餐',
|
||||
packageAmount: '25.00',
|
||||
description: 'NB-IoT专用套餐',
|
||||
frontendSort: 3,
|
||||
sort: 3,
|
||||
maxMonthlyRecharge: 100,
|
||||
maxNextMonthRecharge: 100,
|
||||
monthlyNextMonthAddon: '不支持',
|
||||
status: '停用',
|
||||
levelOneMerge: '否',
|
||||
levelOneMergeCode: '',
|
||||
levelTwoMerge: '否',
|
||||
levelTwoMergeCode: '',
|
||||
isSpecial: '是',
|
||||
creator: '李佳音',
|
||||
modifier: '李佳音',
|
||||
addTime: '2025-11-06 09:45:00',
|
||||
balanceRechargeGradient: '10,50,100',
|
||||
packageCode: 'PKG_003'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
packageSeries: '大流量系列',
|
||||
packageName: '100G全国流量月卡套餐',
|
||||
packageTraffic: 100,
|
||||
packageCount: 1200,
|
||||
packageCountType: '张',
|
||||
packageType: '月套餐',
|
||||
packageAmount: '59.00',
|
||||
description: '100GB大流量月卡',
|
||||
frontendSort: 4,
|
||||
sort: 4,
|
||||
maxMonthlyRecharge: 80,
|
||||
maxNextMonthRecharge: 80,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '是',
|
||||
levelOneMergeCode: 'L1_003',
|
||||
levelTwoMerge: '否',
|
||||
levelTwoMergeCode: '',
|
||||
isSpecial: '否',
|
||||
creator: '赵强',
|
||||
modifier: '赵强',
|
||||
addTime: '2025-11-05 16:20:00',
|
||||
balanceRechargeGradient: '20,50,100,200',
|
||||
packageCode: 'PKG_004'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
packageSeries: '广电系列',
|
||||
packageName: '广电飞悦卡无预存50G',
|
||||
packageTraffic: 50,
|
||||
packageCount: 600,
|
||||
packageCountType: '张',
|
||||
packageType: '月套餐',
|
||||
packageAmount: '39.00',
|
||||
description: '广电30天50GB流量',
|
||||
frontendSort: 5,
|
||||
sort: 5,
|
||||
maxMonthlyRecharge: 60,
|
||||
maxNextMonthRecharge: 60,
|
||||
monthlyNextMonthAddon: '支持',
|
||||
status: '启用',
|
||||
levelOneMerge: '否',
|
||||
levelOneMergeCode: '',
|
||||
levelTwoMerge: '是',
|
||||
levelTwoMergeCode: 'L2_002',
|
||||
isSpecial: '是',
|
||||
creator: '张若暄',
|
||||
modifier: '张若暄',
|
||||
addTime: '2025-11-04 11:30:00',
|
||||
balanceRechargeGradient: '10,30,50',
|
||||
packageCode: 'PKG_005'
|
||||
}
|
||||
]
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '套餐类型',
|
||||
prop: 'packageType',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '月套餐', value: 'monthly' },
|
||||
{ label: '年套餐', value: 'yearly' },
|
||||
{ label: '流量包', value: 'data_package' },
|
||||
{ label: '季度套餐', value: 'quarterly' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐名称',
|
||||
prop: 'packageName',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐名称'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐编码',
|
||||
prop: 'packageCode',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐编码'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 'active' },
|
||||
{ label: '停用', value: 'inactive' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '套餐系列',
|
||||
prop: 'packageSeries',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入套餐系列'
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '一级合并',
|
||||
prop: 'levelOneMerge',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: 'yes' },
|
||||
{ label: '否', value: 'no' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '二级合并',
|
||||
prop: 'levelTwoMerge',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '是', value: 'yes' },
|
||||
{ label: '否', value: 'no' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '勾选', type: 'selection' },
|
||||
{ label: '套餐系列', prop: 'packageSeries' },
|
||||
{ label: '套餐名称', prop: 'packageName' },
|
||||
{ label: '套餐流量(GB)', prop: 'packageTraffic' },
|
||||
{ label: '套餐数量', prop: 'packageCount' },
|
||||
{ label: '套餐数量类型', prop: 'packageCountType' },
|
||||
{ label: '套餐类型', prop: 'packageType' },
|
||||
{ label: '套餐金额', prop: 'packageAmount' },
|
||||
{ label: '描述', prop: 'description' },
|
||||
{ label: '前端排序', prop: 'frontendSort' },
|
||||
{ label: '排序', prop: 'sort' },
|
||||
{ label: '本月最大充值数量', prop: 'maxMonthlyRecharge' },
|
||||
{ label: '次月最大充值数量', prop: 'maxNextMonthRecharge' },
|
||||
{ label: '本月次月加餐包', prop: 'monthlyNextMonthAddon' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '一级是否合并', prop: 'levelOneMerge' },
|
||||
{ label: '一级合并编号', prop: 'levelOneMergeCode' },
|
||||
{ label: '二级是否合并', prop: 'levelTwoMerge' },
|
||||
{ label: '二级合并编号', prop: 'levelTwoMergeCode' },
|
||||
{ label: '是否特殊', prop: 'isSpecial' },
|
||||
{ label: '创建人', prop: 'creator' },
|
||||
{ label: '修改人', prop: 'modifier' },
|
||||
{ label: '添加时间', prop: 'addTime' },
|
||||
{ label: '余额充值梯度', prop: 'balanceRechargeGradient' },
|
||||
{ label: '套餐编码', prop: 'packageCode' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
]
|
||||
|
||||
// 描述字段配置(用于卡片视图)
|
||||
const descriptionsFields = [
|
||||
{ prop: 'packageName', label: '套餐名称', span: 2 },
|
||||
{ prop: 'packageSeries', label: '套餐系列' },
|
||||
{ prop: 'packageCode', label: '套餐编码' },
|
||||
{
|
||||
prop: 'packageTraffic',
|
||||
label: '套餐流量',
|
||||
formatter: (row: any) => `${row.packageTraffic}GB`
|
||||
},
|
||||
{
|
||||
prop: 'packageCount',
|
||||
label: '套餐数量',
|
||||
formatter: (row: any) => `${row.packageCount}${row.packageCountType}`
|
||||
},
|
||||
{ prop: 'packageType', label: '套餐类型' },
|
||||
{
|
||||
prop: 'packageAmount',
|
||||
label: '套餐金额',
|
||||
formatter: (row: any) => `¥${row.packageAmount}`
|
||||
},
|
||||
{ prop: 'description', label: '描述', span: 2 },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row: any) => {
|
||||
const type = getStatusType(row.status)
|
||||
return `<el-tag type="${type}" size="small">${row.status}</el-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'monthlyNextMonthAddon',
|
||||
label: '本月次月加餐包',
|
||||
formatter: (row: any) => {
|
||||
const type = row.monthlyNextMonthAddon === '支持' ? 'success' : 'info'
|
||||
return `<el-tag type="${type}" size="small">${row.monthlyNextMonthAddon}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'maxMonthlyRecharge', label: '本月最大充值数量' },
|
||||
{ prop: 'maxNextMonthRecharge', label: '次月最大充值数量' },
|
||||
{
|
||||
prop: 'levelOneMerge',
|
||||
label: '一级是否合并',
|
||||
formatter: (row: any) => {
|
||||
const type = getMergeType(row.levelOneMerge)
|
||||
return `<el-tag type="${type}" size="small">${row.levelOneMerge}</el-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelOneMergeCode',
|
||||
label: '一级合并编号',
|
||||
formatter: (row: any) => row.levelOneMergeCode || '-'
|
||||
},
|
||||
{
|
||||
prop: 'levelTwoMerge',
|
||||
label: '二级是否合并',
|
||||
formatter: (row: any) => {
|
||||
const type = getMergeType(row.levelTwoMerge)
|
||||
return `<el-tag type="${type}" size="small">${row.levelTwoMerge}</el-tag>`
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelTwoMergeCode',
|
||||
label: '二级合并编号',
|
||||
formatter: (row: any) => row.levelTwoMergeCode || '-'
|
||||
},
|
||||
{
|
||||
prop: 'isSpecial',
|
||||
label: '是否特殊',
|
||||
formatter: (row: any) => {
|
||||
const type = getSpecialType(row.isSpecial)
|
||||
return `<el-tag type="${type}" size="small">${row.isSpecial}</el-tag>`
|
||||
}
|
||||
},
|
||||
{ prop: 'creator', label: '创建人' },
|
||||
{ prop: 'addTime', label: '添加时间', span: 2 },
|
||||
{ prop: 'balanceRechargeGradient', label: '余额充值梯度' },
|
||||
{ prop: 'frontendSort', label: '前端排序' },
|
||||
{ prop: 'sort', label: '排序' }
|
||||
]
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case '启用':
|
||||
return 'success'
|
||||
case '停用':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取是否合并标签类型
|
||||
const getMergeType = (merge: string) => {
|
||||
switch (merge) {
|
||||
case '是':
|
||||
return 'success'
|
||||
case '否':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取是否特殊标签类型
|
||||
const getSpecialType = (special: string) => {
|
||||
switch (special) {
|
||||
case '是':
|
||||
return 'warning'
|
||||
case '否':
|
||||
return 'info'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const exportExcel = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要导出的数据')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`导出 ${selectedRows.value.length} 条套餐记录`)
|
||||
}
|
||||
|
||||
// 显示新增对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogVisible.value = true
|
||||
// 重置表单
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
formData.series = ''
|
||||
formData.balanceRechargeGradient = ''
|
||||
formData.name = ''
|
||||
formData.type = ''
|
||||
formData.monthlyNextMonthAddon = ''
|
||||
formData.maxMonthlyRecharge = 0
|
||||
formData.maxNextMonthRecharge = 0
|
||||
formData.packageCount = 0
|
||||
formData.costPrice = 0
|
||||
formData.packageAmount = 0
|
||||
}
|
||||
|
||||
// 编辑套餐
|
||||
const editPackage = (row: any) => {
|
||||
ElMessage.info(`编辑套餐: ${row.packageName}`)
|
||||
}
|
||||
|
||||
// 下架套餐
|
||||
const offlinePackage = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要下架套餐"${row.packageName}"吗?`, '下架确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('套餐下架成功')
|
||||
getPackageList()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消下架')
|
||||
})
|
||||
}
|
||||
|
||||
// 复制套餐
|
||||
const copyPackage = (row: any) => {
|
||||
ElMessage.info(`复制套餐: ${row.packageName}`)
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' },
|
||||
{
|
||||
prop: 'packageSeries',
|
||||
label: '套餐系列',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'packageName',
|
||||
label: '套餐名称',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'packageTraffic',
|
||||
label: '套餐流量(GB)',
|
||||
width: 120,
|
||||
formatter: (row) => `${row.packageTraffic}GB`
|
||||
},
|
||||
{
|
||||
prop: 'packageCount',
|
||||
label: '套餐数量',
|
||||
width: 100,
|
||||
formatter: (row) => `${row.packageCount}${row.packageCountType}`
|
||||
},
|
||||
{
|
||||
prop: 'packageType',
|
||||
label: '套餐类型',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'packageAmount',
|
||||
label: '套餐金额',
|
||||
width: 100,
|
||||
formatter: (row) => `¥${row.packageAmount}`
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelOneMerge',
|
||||
label: '一级是否合并',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getMergeType(row.levelOneMerge) }, () => row.levelOneMerge)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelOneMergeCode',
|
||||
label: '一级合并编号',
|
||||
width: 120,
|
||||
formatter: (row) => row.levelOneMergeCode || '-'
|
||||
},
|
||||
{
|
||||
prop: 'levelTwoMerge',
|
||||
label: '二级是否合并',
|
||||
width: 120,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getMergeType(row.levelTwoMerge) }, () => row.levelTwoMerge)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'levelTwoMergeCode',
|
||||
label: '二级合并编号',
|
||||
width: 120,
|
||||
formatter: (row) => row.levelTwoMergeCode || '-'
|
||||
},
|
||||
{
|
||||
prop: 'isSpecial',
|
||||
label: '是否特殊',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getSpecialType(row.isSpecial) }, () => row.isSpecial)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'creator',
|
||||
label: '创建人',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'addTime',
|
||||
label: '添加时间',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
prop: 'packageCode',
|
||||
label: '套餐编码',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 240,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'operation-buttons' }, [
|
||||
h(ArtButtonTable, {
|
||||
text: '编辑',
|
||||
onClick: () => editPackage(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '下架',
|
||||
onClick: () => offlinePackage(row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
text: '复制套餐',
|
||||
onClick: () => copyPackage(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getPackageList()
|
||||
})
|
||||
|
||||
// 获取套餐列表
|
||||
const getPackageList = 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
|
||||
} catch (error) {
|
||||
console.error('获取套餐列表失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPackageList()
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
const handleViewChange = (view: string) => {
|
||||
console.log('视图切换到:', view)
|
||||
// 可以在这里添加视图切换的额外逻辑,比如保存用户偏好
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
series: [{ required: true, message: '请选择套餐系列', trigger: 'change' }],
|
||||
balanceRechargeGradient: [{ required: true, message: '请选择余额充值梯度', trigger: 'change' }],
|
||||
name: [
|
||||
{ required: true, message: '请输入套餐名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '套餐名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }],
|
||||
monthlyNextMonthAddon: [{ required: true, message: '请选择本月次月加餐包', trigger: 'change' }],
|
||||
maxMonthlyRecharge: [{ required: true, message: '请输入本月最大充值数量', trigger: 'blur' }],
|
||||
maxNextMonthRecharge: [{ required: true, message: '请输入次月最大充值数量', trigger: 'blur' }],
|
||||
packageCount: [{ required: true, message: '请输入套餐数量', trigger: 'blur' }],
|
||||
costPrice: [{ required: true, message: '请输入成本价', trigger: 'blur' }],
|
||||
packageAmount: [{ required: true, message: '请输入套餐金额', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟提交过程
|
||||
setTimeout(() => {
|
||||
ElMessage.success(
|
||||
`新增套餐成功!套餐名称:${formData.name},套餐系列:${formData.series}`
|
||||
)
|
||||
dialogVisible.value = false
|
||||
submitLoading.value = false
|
||||
getPackageList()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.package-create-page {
|
||||
// 可以添加特定样式
|
||||
}
|
||||
|
||||
:deep(.operation-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,9 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<el-button type="primary" @click="showCreateDialog">新增规则</el-button>
|
||||
<el-button type="primary" @click="showCreateDialog" v-if="hasAuth('alert_rules:create')"
|
||||
>新增规则</el-button
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -20,26 +22,13 @@
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
:actions="getActions"
|
||||
:actionsWidth="160"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑规则弹窗 -->
|
||||
@@ -164,26 +153,16 @@
|
||||
import { PollingAlertService } from '@/api/modules'
|
||||
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingAlertRule[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref()
|
||||
const currentRow = ref<PollingAlertRule | null>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
|
||||
rule_name: '',
|
||||
@@ -301,14 +280,38 @@
|
||||
}))
|
||||
)
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
// 获取操作按钮
|
||||
const getActions = (row: PollingAlertRule) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 禁用/启用按钮直接显示
|
||||
if (hasAuth('alert_rules:toggle')) {
|
||||
actions.push({
|
||||
label: row.status === 1 ? '禁用' : '启用',
|
||||
handler: () => toggleStatus(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑和删除放到更多里
|
||||
if (hasAuth('alert_rules:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showEditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('alert_rules:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleDelete(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
@@ -444,31 +447,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showEditDialog(currentRow.value)
|
||||
break
|
||||
case 'toggle':
|
||||
toggleStatus(currentRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,9 +27,18 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showCreateDialog">新增配置</ElButton>
|
||||
<ElButton @click="handlePreview">预览清理</ElButton>
|
||||
<ElButton type="warning" @click="handleTriggerCleanup" :loading="triggering">
|
||||
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('data_cleanup:create')"
|
||||
>新增配置</ElButton
|
||||
>
|
||||
<ElButton @click="handlePreview" v-if="hasAuth('data_cleanup:preview')"
|
||||
>预览清理</ElButton
|
||||
>
|
||||
<ElButton
|
||||
type="warning"
|
||||
@click="handleTriggerCleanup"
|
||||
:loading="triggering"
|
||||
v-if="hasAuth('data_cleanup:trigger')"
|
||||
>
|
||||
手动清理
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -42,28 +51,15 @@
|
||||
:loading="loading"
|
||||
:data="configList"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:show-pagination="false"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -131,8 +127,8 @@
|
||||
<ElTable :data="previewData" border>
|
||||
<ElTableColumn prop="table_name" label="表名" width="150" />
|
||||
<ElTableColumn prop="description" label="说明" min-width="180" />
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
|
||||
<ElTableColumn prop="record_count" label="待清理记录数" width="130" align="right">
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" />
|
||||
<ElTableColumn prop="record_count" label="待清理记录数" width="130">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-danger': row.record_count > 10000 }">
|
||||
{{ row.record_count.toLocaleString() }}
|
||||
@@ -146,7 +142,7 @@
|
||||
</ElDialog>
|
||||
|
||||
<!-- 清理日志对话框 -->
|
||||
<ElDialog v-model="logVisible" title="清理日志" width="1000px">
|
||||
<ElDialog v-model="logVisible" title="清理日志" width="60%">
|
||||
<div style="margin-bottom: 16px">
|
||||
<ElInput
|
||||
v-model="logFilter.table_name"
|
||||
@@ -183,14 +179,14 @@
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
|
||||
<ElTableColumn prop="deleted_count" label="删除记录数" width="110" align="right">
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" />
|
||||
<ElTableColumn prop="deleted_count" label="删除记录数" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ row.deleted_count.toLocaleString() }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="duration_ms" label="耗时(ms)" width="100" align="right" />
|
||||
<ElTableColumn prop="started_at" label="开始时间" width="160">
|
||||
<ElTableColumn prop="duration_ms" label="耗时(ms)" width="100" />
|
||||
<ElTableColumn prop="started_at" label="开始时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.started_at) }}
|
||||
</template>
|
||||
@@ -235,14 +231,13 @@
|
||||
DataCleanupProgress
|
||||
} from '@/types/api'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
defineOptions({ name: 'DataCleanup' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const triggering = ref(false)
|
||||
@@ -253,10 +248,6 @@
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 右键菜单
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<DataCleanupConfig | null>(null)
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '表名', prop: 'table_name' },
|
||||
@@ -329,14 +320,12 @@
|
||||
{
|
||||
prop: 'retention_days',
|
||||
label: '保留天数',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'batch_size',
|
||||
label: '每批删除条数',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
formatter: (row: DataCleanupConfig) => row.batch_size.toLocaleString()
|
||||
},
|
||||
{
|
||||
@@ -429,7 +418,7 @@
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
await nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
@@ -537,14 +526,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 查看日志
|
||||
const handleViewLogs = () => {
|
||||
logFilter.table_name = ''
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
}
|
||||
|
||||
// 获取清理日志
|
||||
const getCleanupLogs = async () => {
|
||||
try {
|
||||
@@ -562,51 +543,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'logs', label: '查看日志' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
// 获取操作按钮
|
||||
const getActions = (row: DataCleanupConfig) => {
|
||||
const actions: any[] = []
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: DataCleanupConfig, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showEditDialog(currentRow.value)
|
||||
break
|
||||
case 'logs':
|
||||
logFilter.table_name = currentRow.value.table_name
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentRow.value)
|
||||
break
|
||||
// 查看日志按钮直接显示
|
||||
if (hasAuth('data_cleanup:logs')) {
|
||||
actions.push({
|
||||
label: '查看日志',
|
||||
handler: () => {
|
||||
logFilter.table_name = row.table_name
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
},
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
// 编辑和删除放到更多里
|
||||
if (hasAuth('data_cleanup:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showEditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('data_cleanup:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleDelete(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -614,10 +587,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
|
||||
@@ -1,81 +1,5 @@
|
||||
<template>
|
||||
<div class="polling-monitor-page">
|
||||
<!-- 总览统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon total">
|
||||
<el-icon :size="32"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">总卡数</div>
|
||||
<div class="stat-value">{{ overviewStats?.total_cards || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon initialized">
|
||||
<el-icon :size="32"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已初始化</div>
|
||||
<div class="stat-value">{{ overviewStats?.initialized_cards || 0 }}</div>
|
||||
<el-progress
|
||||
v-if="overviewStats"
|
||||
:percentage="overviewStats.init_progress"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
style="margin-top: 8px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon realname">
|
||||
<el-icon :size="32"><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">实名队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.realname_queue_size || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon package">
|
||||
<el-icon :size="32"><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">套餐队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.package_queue_size || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon carddata">
|
||||
<el-icon :size="32"><DataLine /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">流量队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.carddata_queue_size || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 队列状态 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
@@ -173,33 +97,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Document, CircleCheck, User, Box, DataLine } from '@element-plus/icons-vue'
|
||||
import { PollingMonitorService } from '@/api/modules'
|
||||
import type {
|
||||
PollingOverviewStats,
|
||||
PollingInitProgress,
|
||||
PollingQueueStatus,
|
||||
PollingTaskStats
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
const overviewStats = ref<PollingOverviewStats>()
|
||||
const initProgress = ref<PollingInitProgress>()
|
||||
const queueStatusList = ref<PollingQueueStatus[]>([])
|
||||
const taskStatsList = ref<PollingTaskStats[]>([])
|
||||
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 加载总览统计
|
||||
const loadOverviewStats = async () => {
|
||||
try {
|
||||
const { data } = await PollingMonitorService.getOverviewStats()
|
||||
overviewStats.value = data
|
||||
} catch (error) {
|
||||
ElMessage.error('加载总览统计失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载初始化进度
|
||||
const loadInitProgress = async (showMessage = false) => {
|
||||
try {
|
||||
@@ -244,7 +154,6 @@
|
||||
|
||||
// 加载所有数据
|
||||
const loadAllData = () => {
|
||||
loadOverviewStats()
|
||||
loadInitProgress()
|
||||
loadQueueStatus()
|
||||
loadTaskStats()
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 搜索和操作区 -->
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="产品名称/ICCID" clearable />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="operatorFilter" placeholder="运营商筛选" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="中国移动" value="CMCC" />
|
||||
<ElOption label="中国联通" value="CUCC" />
|
||||
<ElOption label="中国电信" value="CTCC" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="statusFilter" placeholder="分配状态" clearable style="width: 100%">
|
||||
<ElOption label="全部" value="" />
|
||||
<ElOption label="已分配" value="assigned" />
|
||||
<ElOption label="未分配" value="unassigned" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple type="primary" @click="showAssignDialog">批量分配</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 号卡产品列表 -->
|
||||
<ArtTable
|
||||
:data="filteredData"
|
||||
index
|
||||
style="margin-top: 20px"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn label="产品名称" prop="productName" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="运营商" prop="operator" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getOperatorTagType(scope.row.operator)">
|
||||
{{ getOperatorText(scope.row.operator) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐规格" prop="packageSpec" min-width="150" />
|
||||
<ElTableColumn label="产品价格" prop="price" width="120">
|
||||
<template #default="scope"> ¥{{ scope.row.price.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="库存数量" prop="stock" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.stock < 100 ? 'var(--el-color-danger)' : '' }">
|
||||
{{ scope.row.stock }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="已分配数量" prop="assignedCount" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<span style="color: var(--el-color-primary)">{{ scope.row.assignedCount }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分配状态" prop="assignStatus" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.assignedCount > 0" type="success">已分配</ElTag>
|
||||
<ElTag v-else type="info">未分配</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link :icon="View" @click="viewAssignDetail(scope.row)">分配记录</el-button>
|
||||
<el-button link type="primary" @click="assignToAgent(scope.row)">分配</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 分配对话框 -->
|
||||
<ElDialog v-model="assignDialogVisible" title="分配号卡产品" width="600px" align-center>
|
||||
<ElForm ref="formRef" :model="assignForm" :rules="assignRules" label-width="120px">
|
||||
<ElFormItem label="选择代理商" prop="agentId">
|
||||
<ElSelect
|
||||
v-model="assignForm.agentId"
|
||||
placeholder="请选择代理商"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleAgentChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="agent in agentList"
|
||||
:key="agent.id"
|
||||
:label="`${agent.agentName} (${agent.phone})`"
|
||||
:value="agent.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分配数量" prop="quantity">
|
||||
<ElInputNumber
|
||||
v-model="assignForm.quantity"
|
||||
:min="1"
|
||||
:max="currentProduct.stock"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
当前库存:{{ currentProduct.stock }} 张
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分佣模式" prop="commissionMode">
|
||||
<ElRadioGroup v-model="assignForm.commissionMode">
|
||||
<ElRadio value="fixed">固定佣金</ElRadio>
|
||||
<ElRadio value="percent">比例佣金</ElRadio>
|
||||
<ElRadio value="template">使用模板</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="assignForm.commissionMode === 'fixed'"
|
||||
label="固定金额"
|
||||
prop="fixedAmount"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="assignForm.fixedAmount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px">元/张</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="assignForm.commissionMode === 'percent'" label="佣金比例" prop="percent">
|
||||
<ElInputNumber
|
||||
v-model="assignForm.percent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="assignForm.commissionMode === 'template'"
|
||||
label="分佣模板"
|
||||
prop="templateId"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="assignForm.templateId"
|
||||
placeholder="请选择分佣模板"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="template in commissionTemplates"
|
||||
:key="template.id"
|
||||
:label="template.templateName"
|
||||
:value="template.id"
|
||||
>
|
||||
<span>{{ template.templateName }}</span>
|
||||
<span style="float: right; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ template.mode === 'fixed' ? `¥${template.value}元/张` : `${template.value}%` }}
|
||||
</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="特殊折扣" prop="discount">
|
||||
<ElInputNumber
|
||||
v-model="assignForm.discount"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px">%</span>
|
||||
<div style="margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
0表示无折扣,设置后代理商可以此折扣价格销售
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="assignForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="assignDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleAssignSubmit">确认分配</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 分配记录对话框 -->
|
||||
<ElDialog v-model="detailDialogVisible" title="分配记录" width="900px" align-center>
|
||||
<ArtTable :data="assignRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="代理商名称" prop="agentName" min-width="150" />
|
||||
<ElTableColumn label="分配数量" prop="quantity" width="100" align="center" />
|
||||
<ElTableColumn label="分佣模式" prop="commissionMode" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag v-if="scope.row.commissionMode === 'fixed'" type="warning">固定佣金</ElTag>
|
||||
<ElTag v-else-if="scope.row.commissionMode === 'percent'" type="success"
|
||||
>比例佣金</ElTag
|
||||
>
|
||||
<ElTag v-else>模板佣金</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金规则" prop="commissionRule" width="150" />
|
||||
<ElTableColumn label="特殊折扣" prop="discount" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.discount > 0 ? `${scope.row.discount}%` : '无' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="分配时间" prop="assignTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" width="100" />
|
||||
<ElTableColumn fixed="right" label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button link type="danger" @click="handleCancelAssign(scope.row)"
|
||||
>取消分配</el-button
|
||||
>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimCardAssign' })
|
||||
|
||||
interface SimCardProduct {
|
||||
id: string
|
||||
productName: string
|
||||
operator: string
|
||||
packageSpec: string
|
||||
price: number
|
||||
stock: number
|
||||
assignedCount: number
|
||||
}
|
||||
|
||||
const searchQuery = ref('')
|
||||
const operatorFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
const assignDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const selectedRows = ref<SimCardProduct[]>([])
|
||||
|
||||
const currentProduct = ref<SimCardProduct>({
|
||||
id: '',
|
||||
productName: '',
|
||||
operator: '',
|
||||
packageSpec: '',
|
||||
price: 0,
|
||||
stock: 0,
|
||||
assignedCount: 0
|
||||
})
|
||||
|
||||
const assignForm = reactive({
|
||||
agentId: '',
|
||||
quantity: 1,
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 10,
|
||||
templateId: '',
|
||||
discount: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const assignRules = reactive<FormRules>({
|
||||
agentId: [{ required: true, message: '请选择代理商', trigger: 'change' }],
|
||||
quantity: [{ required: true, message: '请输入分配数量', trigger: 'blur' }],
|
||||
commissionMode: [{ required: true, message: '请选择分佣模式', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const agentList = ref([
|
||||
{ id: '1', agentName: '华东区总代理', phone: '13800138000', level: 1 },
|
||||
{ id: '2', agentName: '华南区代理', phone: '13900139000', level: 2 },
|
||||
{ id: '3', agentName: '华北区代理', phone: '13700137000', level: 1 }
|
||||
])
|
||||
|
||||
const commissionTemplates = ref([
|
||||
{ id: '1', templateName: '标准代理商佣金', mode: 'percent', value: 10 },
|
||||
{ id: '2', templateName: '特殊套餐固定佣金', mode: 'fixed', value: 50 },
|
||||
{ id: '3', templateName: '高端代理商佣金', mode: 'percent', value: 15 }
|
||||
])
|
||||
|
||||
const mockData = ref<SimCardProduct[]>([
|
||||
{
|
||||
id: '1',
|
||||
productName: '移动4G流量卡-月包100GB',
|
||||
operator: 'CMCC',
|
||||
packageSpec: '100GB/月,有效期1年',
|
||||
price: 80.0,
|
||||
stock: 1000,
|
||||
assignedCount: 500
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
productName: '联通5G流量卡-季包300GB',
|
||||
operator: 'CUCC',
|
||||
packageSpec: '300GB/季,有效期1年',
|
||||
price: 220.0,
|
||||
stock: 500,
|
||||
assignedCount: 200
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
productName: '电信物联网卡-年包1TB',
|
||||
operator: 'CTCC',
|
||||
packageSpec: '1TB/年,有效期2年',
|
||||
price: 800.0,
|
||||
stock: 80,
|
||||
assignedCount: 0
|
||||
}
|
||||
])
|
||||
|
||||
const assignRecords = ref([
|
||||
{
|
||||
id: '1',
|
||||
agentName: '华东区总代理',
|
||||
quantity: 200,
|
||||
commissionMode: 'percent',
|
||||
commissionRule: '10%',
|
||||
discount: 5,
|
||||
assignTime: '2026-01-08 10:00:00',
|
||||
operator: 'admin'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
agentName: '华南区代理',
|
||||
quantity: 150,
|
||||
commissionMode: 'fixed',
|
||||
commissionRule: '¥5.00/张',
|
||||
discount: 0,
|
||||
assignTime: '2026-01-07 14:30:00',
|
||||
operator: 'admin'
|
||||
}
|
||||
])
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
data = data.filter((item) => item.productName.includes(searchQuery.value))
|
||||
}
|
||||
|
||||
if (operatorFilter.value) {
|
||||
data = data.filter((item) => item.operator === operatorFilter.value)
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
if (statusFilter.value === 'assigned') {
|
||||
data = data.filter((item) => item.assignedCount > 0)
|
||||
} else if (statusFilter.value === 'unassigned') {
|
||||
data = data.filter((item) => item.assignedCount === 0)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const getOperatorText = (operator: string) => {
|
||||
const map: Record<string, string> = {
|
||||
CMCC: '中国移动',
|
||||
CUCC: '中国联通',
|
||||
CTCC: '中国电信'
|
||||
}
|
||||
return map[operator] || operator
|
||||
}
|
||||
|
||||
const getOperatorTagType = (operator: string) => {
|
||||
const map: Record<string, any> = {
|
||||
CMCC: 'success',
|
||||
CUCC: 'primary',
|
||||
CTCC: 'warning'
|
||||
}
|
||||
return map[operator] || ''
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const handleSelectionChange = (rows: SimCardProduct[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const showAssignDialog = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要分配的产品')
|
||||
return
|
||||
}
|
||||
if (selectedRows.value.length > 1) {
|
||||
ElMessage.warning('批量分配功能开发中,请单个选择')
|
||||
return
|
||||
}
|
||||
currentProduct.value = selectedRows.value[0]
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
const assignToAgent = (row: SimCardProduct) => {
|
||||
currentProduct.value = row
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleAgentChange = () => {
|
||||
// 可以根据代理商自动填充默认佣金设置
|
||||
}
|
||||
|
||||
const handleAssignSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (assignForm.quantity > currentProduct.value.stock) {
|
||||
ElMessage.error('分配数量不能超过库存数量')
|
||||
return
|
||||
}
|
||||
|
||||
// 更新分配数量
|
||||
currentProduct.value.assignedCount += assignForm.quantity
|
||||
currentProduct.value.stock -= assignForm.quantity
|
||||
|
||||
assignDialogVisible.value = false
|
||||
formRef.value.resetFields()
|
||||
ElMessage.success('分配成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewAssignDetail = (row: SimCardProduct) => {
|
||||
currentProduct.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCancelAssign = (row: any) => {
|
||||
ElMessageBox.confirm('取消分配后,该代理商将无法继续销售此产品,确定取消吗?', '取消分配', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 恢复库存
|
||||
currentProduct.value.stock += row.quantity
|
||||
currentProduct.value.assignedCount -= row.quantity
|
||||
|
||||
const index = assignRecords.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) assignRecords.value.splice(index, 1)
|
||||
ElMessage.success('取消分配成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-select-dropdown__item) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,286 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="号卡名称/编码" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElSelect v-model="operatorFilter" placeholder="运营商" clearable style="width: 100%">
|
||||
<ElOption label="中国移动" value="cmcc" />
|
||||
<ElOption label="中国联通" value="cucc" />
|
||||
<ElOption label="中国电信" value="ctcc" />
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增号卡</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="号卡名称" prop="cardName" min-width="150" />
|
||||
<ElTableColumn label="号卡编码" prop="cardCode" />
|
||||
<ElTableColumn label="运营商" prop="operator">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getOperatorTagType(scope.row.operator)">
|
||||
{{ getOperatorText(scope.row.operator) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="套餐类型" prop="packageType" />
|
||||
<ElTableColumn label="月租(元)" prop="monthlyFee">
|
||||
<template #default="scope"> ¥{{ scope.row.monthlyFee.toFixed(2) }} </template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="库存" prop="stock" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'online' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'online' ? '上架' : '下架' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'online' ? 'danger' : 'primary'"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'online' ? '下架' : '上架' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增号卡' : '编辑号卡'"
|
||||
width="700px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="号卡名称" prop="cardName">
|
||||
<ElInput v-model="form.cardName" placeholder="请输入号卡名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="号卡编码" prop="cardCode">
|
||||
<ElInput v-model="form.cardCode" placeholder="请输入号卡编码" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="运营商" prop="operator">
|
||||
<ElSelect v-model="form.operator" placeholder="请选择运营商" style="width: 100%">
|
||||
<ElOption label="中国移动" value="cmcc" />
|
||||
<ElOption label="中国联通" value="cucc" />
|
||||
<ElOption label="中国电信" value="ctcc" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="套餐类型" prop="packageType">
|
||||
<ElInput v-model="form.packageType" placeholder="例如:流量套餐" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="月租(元)" prop="monthlyFee">
|
||||
<ElInputNumber
|
||||
v-model="form.monthlyFee"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="初始库存" prop="stock">
|
||||
<ElInputNumber v-model="form.stock" :min="0" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="号卡描述" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入号卡描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="online" inactive-value="offline" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">{{
|
||||
form.status === 'online' ? '上架' : '下架'
|
||||
}}</span>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'SimCard' })
|
||||
|
||||
interface SimCard {
|
||||
id?: string
|
||||
cardName: string
|
||||
cardCode: string
|
||||
operator: string
|
||||
packageType: string
|
||||
monthlyFee: number
|
||||
stock: number
|
||||
description?: string
|
||||
status: 'online' | 'offline'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
const mockData = ref<SimCard[]>([
|
||||
{
|
||||
id: '1',
|
||||
cardName: '移动流量卡30GB',
|
||||
cardCode: 'CARD_CMCC_30GB',
|
||||
operator: 'cmcc',
|
||||
packageType: '流量套餐',
|
||||
monthlyFee: 29.9,
|
||||
stock: 1000,
|
||||
description: '移动30GB流量卡,全国通用',
|
||||
status: 'online',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cardName: '联通流量卡50GB',
|
||||
cardCode: 'CARD_CUCC_50GB',
|
||||
operator: 'cucc',
|
||||
packageType: '流量套餐',
|
||||
monthlyFee: 49.9,
|
||||
stock: 800,
|
||||
description: '联通50GB流量卡,全国通用',
|
||||
status: 'online',
|
||||
createTime: '2026-01-02 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const operatorFilter = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<SimCard>({
|
||||
cardName: '',
|
||||
cardCode: '',
|
||||
operator: '',
|
||||
packageType: '',
|
||||
monthlyFee: 0,
|
||||
stock: 0,
|
||||
description: '',
|
||||
status: 'online'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
cardName: [{ required: true, message: '请输入号卡名称', trigger: 'blur' }],
|
||||
cardCode: [{ required: true, message: '请输入号卡编码', trigger: 'blur' }],
|
||||
operator: [{ required: true, message: '请选择运营商', trigger: 'change' }],
|
||||
monthlyFee: [{ required: true, message: '请输入月租', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = mockData.value
|
||||
if (searchQuery.value) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.cardName.includes(searchQuery.value) || item.cardCode.includes(searchQuery.value)
|
||||
)
|
||||
}
|
||||
if (operatorFilter.value) {
|
||||
data = data.filter((item) => item.operator === operatorFilter.value)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const getOperatorText = (operator: string) => {
|
||||
const map: Record<string, string> = { cmcc: '中国移动', cucc: '中国联通', ctcc: '中国电信' }
|
||||
return map[operator] || '未知'
|
||||
}
|
||||
|
||||
const getOperatorTagType = (operator: string) => {
|
||||
const map: Record<string, string> = { cmcc: '', cucc: 'success', ctcc: 'warning' }
|
||||
return map[operator] || 'info'
|
||||
}
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: SimCard) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, row)
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
cardName: '',
|
||||
cardCode: '',
|
||||
operator: '',
|
||||
packageType: '',
|
||||
monthlyFee: 0,
|
||||
stock: 0,
|
||||
description: '',
|
||||
status: 'online'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) mockData.value[index] = { ...form }
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toggleStatus = (row: SimCard) => {
|
||||
const action = row.status === 'online' ? '下架' : '上架'
|
||||
ElMessageBox.confirm(`确定要${action}该号卡吗?`, `${action}确认`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.status = row.status === 'online' ? 'offline' : 'online'
|
||||
ElMessage.success(`${action}成功`)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,287 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content server">
|
||||
<div class="list">
|
||||
<div class="middle">
|
||||
<div class="item" v-for="item in serverList" :key="item.name">
|
||||
<div class="header">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="ip">{{ item.ip }}</span>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="left">
|
||||
<img src="@imgs/safeguard/server.png" alt="服务器" />
|
||||
<ElButtonGroup class="ml-4">
|
||||
<ElButton type="primary" size="default">开机</ElButton>
|
||||
<ElButton type="danger" size="default">关机</ElButton>
|
||||
<ElButton type="warning" size="default">重启</ElButton>
|
||||
</ElButtonGroup>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div>
|
||||
<p>CPU</p>
|
||||
<ElProgress :percentage="item.cup" :text-inside="true" :stroke-width="17" />
|
||||
</div>
|
||||
<div>
|
||||
<p>RAM</p>
|
||||
<ElProgress
|
||||
:percentage="item.memory"
|
||||
status="success"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>SWAP</p>
|
||||
<ElProgress
|
||||
:percentage="item.swap"
|
||||
status="warning"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>DISK</p>
|
||||
<ElProgress
|
||||
:percentage="item.disk"
|
||||
status="success"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
defineOptions({ name: 'SafeguardServer' })
|
||||
|
||||
interface ServerInfo {
|
||||
name: string
|
||||
ip: string
|
||||
cup: number
|
||||
memory: number
|
||||
swap: number
|
||||
disk: number
|
||||
}
|
||||
|
||||
const serverList = reactive<ServerInfo[]>([
|
||||
{
|
||||
name: '开发服务器',
|
||||
ip: '192.168.1.100',
|
||||
cup: 85,
|
||||
memory: 65,
|
||||
swap: 45,
|
||||
disk: 92
|
||||
},
|
||||
{
|
||||
name: '测试服务器',
|
||||
ip: '192.168.1.101',
|
||||
cup: 32,
|
||||
memory: 78,
|
||||
swap: 90,
|
||||
disk: 45
|
||||
},
|
||||
{
|
||||
name: '预发布服务器',
|
||||
ip: '192.168.1.102',
|
||||
cup: 95,
|
||||
memory: 42,
|
||||
swap: 67,
|
||||
disk: 88
|
||||
},
|
||||
{
|
||||
name: '线上服务器',
|
||||
ip: '192.168.1.103',
|
||||
cup: 58,
|
||||
memory: 93,
|
||||
swap: 25,
|
||||
disk: 73
|
||||
}
|
||||
])
|
||||
|
||||
// 生成随机数据的函数
|
||||
function generateRandomValue(min = 0, max = 100): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
// 更新服务器数据
|
||||
function updateServerData() {
|
||||
serverList.forEach((server) => {
|
||||
server.cup = generateRandomValue()
|
||||
server.memory = generateRandomValue()
|
||||
server.swap = generateRandomValue()
|
||||
server.disk = generateRandomValue()
|
||||
})
|
||||
}
|
||||
|
||||
// 修改 timer 类型为 number | null
|
||||
let timer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
timer = window.setInterval(updateServerData, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) {
|
||||
window.clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.server {
|
||||
.list {
|
||||
width: 100%;
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(50% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ip {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
|
||||
.left {
|
||||
margin: 0 40px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
> div {
|
||||
margin: 15px 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 20px;
|
||||
|
||||
.left {
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-ipad-pro) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-phone) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
|
||||
.left {
|
||||
margin: 0;
|
||||
|
||||
img {
|
||||
width: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput v-model="searchQuery" placeholder="模板名称" clearable></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||||
<ElButton v-ripple @click="showDialog('add')">新增模板</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="filteredData" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="模板名称" prop="templateName" />
|
||||
<ElTableColumn label="分佣模式" prop="commissionMode">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.commissionMode === 'fixed' ? '' : 'success'">
|
||||
{{ scope.row.commissionMode === 'fixed' ? '固定佣金' : '比例佣金' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金规则" prop="rule">
|
||||
<template #default="scope">
|
||||
{{
|
||||
scope.row.commissionMode === 'fixed'
|
||||
? `¥${scope.row.fixedAmount.toFixed(2)}/笔`
|
||||
: `${scope.row.percent}%`
|
||||
}}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="适用范围" prop="scope" show-overflow-tooltip />
|
||||
<ElTableColumn label="应用次数" prop="usageCount" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="220">
|
||||
<template #default="scope">
|
||||
<el-button link @click="viewUsage(scope.row)">应用记录</el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增分佣模板' : '编辑分佣模板'"
|
||||
width="600px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="模板名称" prop="templateName">
|
||||
<ElInput v-model="form.templateName" placeholder="请输入模板名称" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分佣模式" prop="commissionMode">
|
||||
<ElRadioGroup v-model="form.commissionMode">
|
||||
<ElRadio value="fixed">固定佣金</ElRadio>
|
||||
<ElRadio value="percent">比例佣金</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.commissionMode === 'fixed'" label="固定金额" prop="fixedAmount">
|
||||
<ElInputNumber v-model="form.fixedAmount" :min="0" :precision="2" style="width: 100%" />
|
||||
<span style="margin-left: 8px">元/笔</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="form.commissionMode === 'percent'" label="佣金比例" prop="percent">
|
||||
<ElInputNumber
|
||||
v-model="form.percent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span style="margin-left: 8px">%</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="适用范围" prop="scope">
|
||||
<ElInput v-model="form.scope" placeholder="例如:全部套餐、特定代理商等" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="分佣说明" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入分佣规则说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" active-value="active" inactive-value="inactive" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 应用记录对话框 -->
|
||||
<ElDialog v-model="usageDialogVisible" title="模板应用记录" width="900px" align-center>
|
||||
<ArtTable :data="usageRecords" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="应用对象" prop="targetName" />
|
||||
<ElTableColumn label="对象类型" prop="targetType">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.targetType === 'agent' ? '' : 'success'">
|
||||
{{ scope.row.targetType === 'agent' ? '代理商' : '套餐' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="应用时间" prop="applyTime" width="180" />
|
||||
<ElTableColumn label="操作人" prop="operator" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '生效中' : '已失效' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'CommissionTemplate' })
|
||||
|
||||
interface Template {
|
||||
id?: string
|
||||
templateName: string
|
||||
commissionMode: 'fixed' | 'percent'
|
||||
fixedAmount: number
|
||||
percent: number
|
||||
scope: string
|
||||
description?: string
|
||||
usageCount?: number
|
||||
status: 'active' | 'inactive'
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
const mockData = ref<Template[]>([
|
||||
{
|
||||
id: '1',
|
||||
templateName: '标准代理商佣金',
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 10,
|
||||
scope: '全部套餐',
|
||||
description: '适用于一级代理商的标准佣金模板',
|
||||
usageCount: 25,
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
templateName: '特殊套餐固定佣金',
|
||||
commissionMode: 'fixed',
|
||||
fixedAmount: 50,
|
||||
percent: 0,
|
||||
scope: '高端套餐系列',
|
||||
description: '适用于高端套餐的固定佣金',
|
||||
usageCount: 8,
|
||||
status: 'active',
|
||||
createTime: '2026-01-05 11:00:00'
|
||||
}
|
||||
])
|
||||
|
||||
const usageRecords = ref([
|
||||
{
|
||||
id: '1',
|
||||
targetName: '华东区总代理',
|
||||
targetType: 'agent',
|
||||
applyTime: '2026-01-02 10:00:00',
|
||||
operator: 'admin',
|
||||
status: 'active'
|
||||
}
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const usageDialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive<Template>({
|
||||
templateName: '',
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 0,
|
||||
scope: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||||
commissionMode: [{ required: true, message: '请选择分佣模式', trigger: 'change' }],
|
||||
fixedAmount: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (form.commissionMode === 'fixed' && value <= 0) {
|
||||
callback(new Error('固定金额必须大于0'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
percent: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (form.commissionMode === 'percent' && (value <= 0 || value > 100)) {
|
||||
callback(new Error('比例必须在0-100之间'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
scope: [{ required: true, message: '请输入适用范围', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchQuery.value) return mockData.value
|
||||
return mockData.value.filter((item) => item.templateName.includes(searchQuery.value))
|
||||
})
|
||||
|
||||
const handleSearch = () => {}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: Template) => {
|
||||
dialogType.value = type
|
||||
dialogVisible.value = true
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, row)
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
templateName: '',
|
||||
commissionMode: 'percent',
|
||||
fixedAmount: 0,
|
||||
percent: 0,
|
||||
scope: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
mockData.value.push({
|
||||
...form,
|
||||
id: Date.now().toString(),
|
||||
usageCount: 0,
|
||||
createTime: new Date().toLocaleString('zh-CN')
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
const index = mockData.value.findIndex((item) => item.id === form.id)
|
||||
if (index !== -1) mockData.value[index] = { ...form }
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: Template) => {
|
||||
ElMessageBox.confirm('确定删除该模板吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) mockData.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const viewUsage = (row: Template) => {
|
||||
usageDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- API密钥管理 -->
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-weight: 500">API密钥管理</span>
|
||||
<ElButton type="primary" size="small" @click="showCreateDialog">生成新密钥</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ArtTable :data="apiKeyList" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="密钥名称" prop="keyName" />
|
||||
<ElTableColumn label="AppKey" prop="appKey" min-width="200">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<code style="color: var(--el-color-primary)">{{ scope.row.appKey }}</code>
|
||||
<ElButton link :icon="CopyDocument" @click="copyToClipboard(scope.row.appKey)" />
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="AppSecret" prop="appSecret" min-width="200">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<code>{{ scope.row.showSecret ? scope.row.appSecret : '••••••••••••••••' }}</code>
|
||||
<ElButton link :icon="View" @click="scope.row.showSecret = !scope.row.showSecret" />
|
||||
<ElButton link :icon="CopyDocument" @click="copyToClipboard(scope.row.appSecret)" />
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="权限" prop="permissions">
|
||||
<template #default="scope">
|
||||
<ElTag
|
||||
v-for="(perm, index) in scope.row.permissions"
|
||||
:key="index"
|
||||
size="small"
|
||||
style="margin-right: 4px"
|
||||
>
|
||||
{{ perm }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button link @click="handleResetKey(scope.row)">重置密钥</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- Webhook配置 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">Webhook配置</span>
|
||||
</template>
|
||||
|
||||
<ElForm :model="webhookForm" label-width="120px" style="max-width: 800px">
|
||||
<ElFormItem label="回调地址">
|
||||
<ElInput v-model="webhookForm.url" placeholder="https://your-domain.com/webhook">
|
||||
<template #prepend>POST</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="签名密钥">
|
||||
<ElInput
|
||||
v-model="webhookForm.secret"
|
||||
:type="showWebhookSecret ? 'text' : 'password'"
|
||||
placeholder="用于验证webhook请求签名"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="showWebhookSecret = !showWebhookSecret" />
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="事件订阅">
|
||||
<ElCheckboxGroup v-model="webhookForm.events">
|
||||
<ElCheckbox value="order.created">订单创建</ElCheckbox>
|
||||
<ElCheckbox value="order.paid">订单支付</ElCheckbox>
|
||||
<ElCheckbox value="card.activated">卡片激活</ElCheckbox>
|
||||
<ElCheckbox value="card.expired">卡片过期</ElCheckbox>
|
||||
<ElCheckbox value="recharge.success">充值成功</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="saveWebhook">保存配置</ElButton>
|
||||
<ElButton @click="testWebhook">测试推送</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
|
||||
<!-- API调用统计 -->
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">API调用统计(最近7天)</span>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">总调用次数</div>
|
||||
<div class="stat-value">12,580</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">成功次数</div>
|
||||
<div class="stat-value" style="color: var(--el-color-success)">12,453</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">失败次数</div>
|
||||
<div class="stat-value" style="color: var(--el-color-danger)">127</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">99.0%</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
|
||||
<!-- 生成密钥对话框 -->
|
||||
<ElDialog v-model="createDialogVisible" title="生成新密钥" width="500px" align-center>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<ElFormItem label="密钥名称" prop="keyName">
|
||||
<ElInput v-model="createForm.keyName" placeholder="请输入密钥名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="权限设置">
|
||||
<ElCheckboxGroup v-model="createForm.permissions">
|
||||
<ElCheckbox value="读取">读取</ElCheckbox>
|
||||
<ElCheckbox value="写入">写入</ElCheckbox>
|
||||
<ElCheckbox value="删除">删除</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleCreateKey">生成</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { CopyDocument, View } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'DeveloperApi' })
|
||||
|
||||
interface ApiKey {
|
||||
id: string
|
||||
keyName: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
permissions: string[]
|
||||
status: 'active' | 'inactive'
|
||||
createTime: string
|
||||
showSecret?: boolean
|
||||
}
|
||||
|
||||
const apiKeyList = ref<ApiKey[]>([
|
||||
{
|
||||
id: '1',
|
||||
keyName: '生产环境密钥',
|
||||
appKey: 'ak_prod_1234567890abcdef',
|
||||
appSecret: 'sk_prod_abcdefghijklmnopqrstuvwxyz123456',
|
||||
permissions: ['读取', '写入'],
|
||||
status: 'active',
|
||||
createTime: '2026-01-01 10:00:00',
|
||||
showSecret: false
|
||||
}
|
||||
])
|
||||
|
||||
const webhookForm = reactive({
|
||||
url: 'https://your-domain.com/webhook',
|
||||
secret: 'webhook_secret_key_123456',
|
||||
events: ['order.created', 'order.paid']
|
||||
})
|
||||
|
||||
const showWebhookSecret = ref(false)
|
||||
const createDialogVisible = ref(false)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
|
||||
const createForm = reactive({
|
||||
keyName: '',
|
||||
permissions: ['读取']
|
||||
})
|
||||
|
||||
const createRules = reactive<FormRules>({
|
||||
keyName: [{ required: true, message: '请输入密钥名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
createForm.keyName = ''
|
||||
createForm.permissions = ['读取']
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
if (!createFormRef.value) return
|
||||
await createFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const newKey: ApiKey = {
|
||||
id: Date.now().toString(),
|
||||
keyName: createForm.keyName,
|
||||
appKey: `ak_${Date.now()}`,
|
||||
appSecret: `sk_${Math.random().toString(36).substring(2)}`,
|
||||
permissions: createForm.permissions,
|
||||
status: 'active',
|
||||
createTime: new Date().toLocaleString('zh-CN'),
|
||||
showSecret: false
|
||||
}
|
||||
apiKeyList.value.push(newKey)
|
||||
createDialogVisible.value = false
|
||||
ElMessage.success('密钥生成成功,请妥善保管')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetKey = (row: ApiKey) => {
|
||||
ElMessageBox.confirm('重置后原密钥将失效,确定要重置吗?', '重置密钥', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
row.appSecret = `sk_${Math.random().toString(36).substring(2)}`
|
||||
ElMessage.success('密钥重置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row: ApiKey) => {
|
||||
ElMessageBox.confirm('删除后无法恢复,确定要删除吗?', '删除密钥', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
const index = apiKeyList.value.findIndex((item) => item.id === row.id)
|
||||
if (index !== -1) apiKeyList.value.splice(index, 1)
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const saveWebhook = () => {
|
||||
ElMessage.success('Webhook配置保存成功')
|
||||
}
|
||||
|
||||
const testWebhook = () => {
|
||||
ElMessage.info('正在发送测试推送...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('测试推送成功')
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.stat-box {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,216 +0,0 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">支付商户配置</span>
|
||||
</template>
|
||||
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="150px"
|
||||
style="max-width: 900px"
|
||||
>
|
||||
<ElDivider content-position="left">基础信息</ElDivider>
|
||||
|
||||
<ElFormItem label="商户名称" prop="merchantName">
|
||||
<ElInput v-model="form.merchantName" placeholder="请输入商户名称" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="商户编号" prop="merchantId">
|
||||
<ElInput v-model="form.merchantId" placeholder="请输入商户编号" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">API配置</ElDivider>
|
||||
|
||||
<ElFormItem label="AppID" prop="appId">
|
||||
<ElInput v-model="form.appId" placeholder="请输入AppID">
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="toggleShow('appId')">
|
||||
{{ showFields.appId ? '隐藏' : '显示' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="AppSecret" prop="appSecret">
|
||||
<ElInput
|
||||
v-model="form.appSecret"
|
||||
:type="showFields.appSecret ? 'text' : 'password'"
|
||||
placeholder="请输入AppSecret"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="toggleShow('appSecret')">
|
||||
{{ showFields.appSecret ? '隐藏' : '显示' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="API密钥" prop="apiKey">
|
||||
<ElInput
|
||||
v-model="form.apiKey"
|
||||
:type="showFields.apiKey ? 'text' : 'password'"
|
||||
placeholder="请输入API密钥"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton :icon="View" @click="toggleShow('apiKey')">
|
||||
{{ showFields.apiKey ? '隐藏' : '显示' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">回调配置</ElDivider>
|
||||
|
||||
<ElFormItem label="支付回调地址" prop="notifyUrl">
|
||||
<ElInput v-model="form.notifyUrl" placeholder="https://your-domain.com/api/notify">
|
||||
<template #prepend>POST</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="退款回调地址" prop="refundNotifyUrl">
|
||||
<ElInput
|
||||
v-model="form.refundNotifyUrl"
|
||||
placeholder="https://your-domain.com/api/refund-notify"
|
||||
>
|
||||
<template #prepend>POST</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">支付方式</ElDivider>
|
||||
|
||||
<ElFormItem label="启用的支付方式">
|
||||
<ElCheckboxGroup v-model="form.paymentMethods">
|
||||
<ElCheckbox value="wechat">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<span style="font-size: 20px; color: #09bb07">💬</span>
|
||||
<span>微信支付</span>
|
||||
</div>
|
||||
</ElCheckbox>
|
||||
<ElCheckbox value="alipay">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<span style="font-size: 20px; color: #1677ff">💳</span>
|
||||
<span>支付宝</span>
|
||||
</div>
|
||||
</ElCheckbox>
|
||||
<ElCheckbox value="bank">
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<span style="font-size: 20px">🏦</span>
|
||||
<span>银行卡</span>
|
||||
</div>
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="测试模式">
|
||||
<ElSwitch v-model="form.testMode" />
|
||||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||||
{{ form.testMode ? '开启(使用沙箱环境)' : '关闭(生产环境)' }}
|
||||
</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleSave">保存配置</ElButton>
|
||||
<ElButton @click="handleTest">测试连接</ElButton>
|
||||
<ElButton @click="resetForm">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span style="font-weight: 500">配置说明</span>
|
||||
</template>
|
||||
<ElAlert type="info" :closable="false">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>AppID/AppSecret</strong>: 从支付服务商后台获取</p>
|
||||
<p><strong>API密钥</strong>: 用于签名验证,请妥善保管</p>
|
||||
<p><strong>回调地址</strong>: 支付完成后,支付平台会向该地址发送支付结果通知</p>
|
||||
<p><strong>测试模式</strong>: 开启后使用沙箱环境,不会产生真实交易</p>
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchant' })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive({
|
||||
merchantName: '某某科技有限公司',
|
||||
merchantId: 'MCH123456789',
|
||||
appId: 'wx1234567890abcdef',
|
||||
appSecret: '********************************',
|
||||
apiKey: '********************************',
|
||||
notifyUrl: 'https://your-domain.com/api/payment/notify',
|
||||
refundNotifyUrl: 'https://your-domain.com/api/payment/refund-notify',
|
||||
paymentMethods: ['wechat', 'alipay'],
|
||||
testMode: true
|
||||
})
|
||||
|
||||
const showFields = reactive({
|
||||
appId: false,
|
||||
appSecret: false,
|
||||
apiKey: false
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
merchantName: [{ required: true, message: '请输入商户名称', trigger: 'blur' }],
|
||||
merchantId: [{ required: true, message: '请输入商户编号', trigger: 'blur' }],
|
||||
appId: [{ required: true, message: '请输入AppID', trigger: 'blur' }],
|
||||
appSecret: [{ required: true, message: '请输入AppSecret', trigger: 'blur' }],
|
||||
apiKey: [{ required: true, message: '请输入API密钥', trigger: 'blur' }],
|
||||
notifyUrl: [
|
||||
{ required: true, message: '请输入支付回调地址', trigger: 'blur' },
|
||||
{ type: 'url', message: '请输入正确的URL地址', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const toggleShow = (field: 'appId' | 'appSecret' | 'apiKey') => {
|
||||
showFields[field] = !showFields[field]
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success('配置保存成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTest = () => {
|
||||
ElMessage.info('正在测试连接...')
|
||||
setTimeout(() => {
|
||||
ElMessage.success('连接测试成功')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 30px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -51,6 +51,7 @@
|
||||
const getProviderTypeText = (type: PaymentProviderType): string => {
|
||||
const typeMap: Record<PaymentProviderType, string> = {
|
||||
wechat: '微信直连',
|
||||
wechat_v2: '微信直连V2',
|
||||
fuiou: '富友支付'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showCreateDialog">新增支付配置</ElButton>
|
||||
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('wechat_config:create')">新增支付配置</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -33,29 +33,16 @@
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:actions="getActions"
|
||||
:actionsWidth="120"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 支付配置操作右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="configOperationMenuRef"
|
||||
:menu-items="configOperationMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleConfigOperationMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 创建/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
@@ -436,7 +423,7 @@
|
||||
import { h, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { WechatConfigService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
WechatConfig,
|
||||
@@ -448,10 +435,6 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
@@ -467,10 +450,6 @@
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 右键菜单
|
||||
const configOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentOperatingConfig = ref<WechatConfig | null>(null)
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
provider_type: undefined as PaymentProviderType | undefined,
|
||||
@@ -533,7 +512,7 @@
|
||||
]
|
||||
|
||||
// URL格式验证规则
|
||||
const urlValidator = (rule: any, value: any, callback: any) => {
|
||||
const urlValidator = (_rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
@@ -692,6 +671,7 @@
|
||||
activeText: '已激活',
|
||||
inactiveText: '未激活',
|
||||
inlinePrompt: true,
|
||||
disabled: !hasAuth('wechat_config:status'),
|
||||
'onUpdate:modelValue': (val) => handleStatusChange(row, val)
|
||||
})
|
||||
}
|
||||
@@ -848,7 +828,7 @@
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
await nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
@@ -956,6 +936,11 @@
|
||||
|
||||
// 激活/停用状态切换
|
||||
const handleStatusChange = async (row: WechatConfig, newStatus: string | number | boolean) => {
|
||||
if (!hasAuth('wechat_config:status')) {
|
||||
ElMessage.warning('您没有修改激活状态的权限')
|
||||
return
|
||||
}
|
||||
|
||||
const newBoolStatus = Boolean(newStatus)
|
||||
const oldStatus = row.is_active
|
||||
row.is_active = newBoolStatus
|
||||
@@ -1002,63 +987,35 @@
|
||||
|
||||
// 处理名称点击
|
||||
const handleNameClick = (row: WechatConfig) => {
|
||||
handleViewDetail(row)
|
||||
}
|
||||
|
||||
// 支付配置操作菜单项配置
|
||||
const configOperationMenuItems = computed((): MenuItemType[] => {
|
||||
const items: MenuItemType[] = []
|
||||
|
||||
// 编辑
|
||||
items.push({
|
||||
key: 'edit',
|
||||
label: '编辑'
|
||||
})
|
||||
|
||||
// 删除
|
||||
items.push({
|
||||
key: 'delete',
|
||||
label: '删除'
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
// 显示支付配置操作右键菜单
|
||||
const showConfigOperationMenu = (e: MouseEvent, row: WechatConfig) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
currentOperatingConfig.value = row
|
||||
configOperationMenuRef.value?.show(e)
|
||||
}
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: WechatConfig, column: any, event: MouseEvent) => {
|
||||
showConfigOperationMenu(event, row)
|
||||
}
|
||||
|
||||
// 处理支付配置操作菜单选择
|
||||
const handleConfigOperationMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentOperatingConfig.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showEditDialog(currentOperatingConfig.value)
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentOperatingConfig.value)
|
||||
break
|
||||
if (hasAuth('wechat_config:detail')) {
|
||||
handleViewDetail(row)
|
||||
} else {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
// 获取操作按钮
|
||||
const getActions = (row: WechatConfig) => {
|
||||
const actions: any[] = []
|
||||
|
||||
if (hasAuth('wechat_config:edit')) {
|
||||
actions.push({
|
||||
label: '编辑',
|
||||
handler: () => showEditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('wechat_config:delete')) {
|
||||
actions.push({
|
||||
label: '删除',
|
||||
handler: () => handleDelete(row),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1066,10 +1023,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
max-height: 600px;
|
||||
padding-right: 10px;
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div class="banners">
|
||||
<h1 class="page-title">基础横幅</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="欢迎回来,管理员!"
|
||||
subtitle="今日系统访问量增长了23%,运行状态良好。"
|
||||
buttonText="查看详情"
|
||||
buttonColor="rgb(var(--art-secondary))"
|
||||
buttonTextColor="#fff"
|
||||
@click="handleBannerClick"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="欢迎使用本系统!"
|
||||
subtitle="这是一个基于Vue3和Element Plus的后台管理系统模板。"
|
||||
buttonText="开始使用"
|
||||
buttonColor="rgb(var(--art-success))"
|
||||
buttonTextColor="#fff"
|
||||
backgroundColor="#D4F1F7"
|
||||
titleColor="#333"
|
||||
subtitleColor="#666"
|
||||
@click="handleBannerClick"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">基础横幅(图片)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="欢迎回来,管理员!"
|
||||
subtitle="今日系统访问量增长了23%,运行状态良好。"
|
||||
buttonText="查看详情"
|
||||
buttonColor="#FF3E76"
|
||||
backgroundColor="#FF80A4"
|
||||
buttonTextColor="#fff"
|
||||
:backgroundImage="icon3"
|
||||
@click="handleBannerClick"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="欢迎使用本系统!"
|
||||
subtitle="这是一个基于Vue3和Element Plus的后台管理系统模板。"
|
||||
buttonText="开始使用"
|
||||
buttonColor="#1385FF"
|
||||
buttonTextColor="#fff"
|
||||
backgroundColor="#88A7FF"
|
||||
:backgroundImage="icon5"
|
||||
@click="handleBannerClick"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">卡片横幅</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
title="系统状态良好"
|
||||
description="所有服务运行正常,无异常情况。"
|
||||
buttonText="查看详情"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
:icon="icon2"
|
||||
title="新消息提醒"
|
||||
description="您有3条未读消息需要处理。"
|
||||
buttonText="立即查看"
|
||||
buttonColor="rgb(var(--art-warning))"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
:icon="icon3"
|
||||
title="数据分析报告"
|
||||
description="本周数据分析报告已生成完毕。"
|
||||
buttonText="下载报告"
|
||||
buttonColor="rgb(var(--art-error))"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
:icon="icon4"
|
||||
title="系统更新提示"
|
||||
description="新版本V2.1.0已发布,建议及时更新。"
|
||||
buttonText="更新"
|
||||
buttonColor="rgb(var(--art-primary))"
|
||||
:showCancel="true"
|
||||
cancelButtonText="取消"
|
||||
cancelButtonColor="#eee"
|
||||
cancelButtonTextColor="#333"
|
||||
@click="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import icon2 from '@imgs/3d/icon2.webp'
|
||||
import icon3 from '@imgs/3d/icon3.webp'
|
||||
import icon4 from '@imgs/3d/icon4.webp'
|
||||
import icon5 from '@imgs/3d/icon7.webp'
|
||||
import { ElRow } from 'element-plus'
|
||||
|
||||
const handleBannerClick = () => {
|
||||
console.log('banner clicked')
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
console.log('confirm clicked')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
console.log('cancel clicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banners {
|
||||
padding-top: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 20px 0 15px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user