Compare commits

..

2 Commits

Author SHA1 Message Date
sexygoat
cc9d2a85a2 chore: merge main branch from remote
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m45s
2026-04-08 19:35:20 +08:00
sexygoat
d1c6588d8f 删除多余代码 2026-04-08 19:31:22 +08:00
110 changed files with 897 additions and 24613 deletions

View File

@@ -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 数据

View File

@@ -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) ✅

View File

@@ -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

View File

@@ -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待对接

View File

@@ -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** 系统显示"暂无数据"提示

View File

@@ -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 阶段(测试优化)待实现

View File

@@ -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.

View File

@@ -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导入 + 设备导入)

View File

@@ -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

View File

@@ -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 在所有功能开发完成后进行

View File

@@ -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客户端已在项目中

View File

@@ -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

View File

@@ -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

View File

@@ -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 更新用户手册(如有需要)

View File

@@ -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 调用

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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(

View File

@@ -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": {

View File

@@ -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

View File

@@ -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: '&#xe715;'
},
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,

View File

@@ -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' // 日历
}
// 主页路由

View File

@@ -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[] // 兼容旧字段
}
// 创建代理充值订单请求

View File

@@ -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 // 支付方式
}

View File

@@ -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']

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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: '&#xe722;',
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>

View File

@@ -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">&#xe6eb;</i>{{ item.collection }}</span>
<span><i class="iconfont-sys">&#xe6e9;</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">&#xe6eb;</i>{{ clickItem.collection }}</span>
<span><i class="iconfont-sys">&#xe6e9;</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>

View File

@@ -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>

View File

@@ -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">&#xe6f7;</i>
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
<div class="line"></div>
<i class="iconfont-sys">&#xe689;</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>

View File

@@ -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:9jpg/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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -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
}
// 取消换货

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
})
//

View File

@@ -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>

View File

@@ -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;
}
}
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
>必填字段ICCIDvirtual_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>

View File

@@ -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>

View File

@@ -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: '广电飞悦卡无预存50G30天',
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>

View File

@@ -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

View File

@@ -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: '广电飞悦卡无预存50G30天',
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>

View File

@@ -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: '广电飞悦卡无预存50G30天',
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>

View File

@@ -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>

View File

@@ -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: '广电飞悦卡无预存50G30天', 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: '广电飞悦卡无预存50G30天',
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>

View File

@@ -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: '广电飞悦卡无预存50G30天',
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: '广电飞悦卡无预存50G30天', 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>

View File

@@ -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: '广电飞悦卡无预存50G30天', 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: '广电飞悦卡无预存50G30天',
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>

View File

@@ -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: '广电飞悦卡无预存50G30天',
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>

View File

@@ -1,52 +0,0 @@
<template>
<div class="page-content">
<h3 class="table-title"><i class="iconfont-sys">&#xe74d;</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>

View File

@@ -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,

View File

@@ -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'

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">&#xe71d;</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">&#xe71e;</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">&#xe720;</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">&#xe71f;</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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()

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -51,6 +51,7 @@
const getProviderTypeText = (type: PaymentProviderType): string => {
const typeMap: Record<PaymentProviderType, string> = {
wechat: '微信直连',
wechat_v2: '微信直连V2',
fuiou: '富友支付'
}
return typeMap[type] || type

View File

@@ -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;

View File

@@ -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