fetch(modify):修改原来的bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m53s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m53s
This commit is contained in:
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||||
<!-- 引入小米字体 CSS 文件 -->
|
<!-- 引入小米字体 CSS 文件 -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css"/>
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css"
|
||||||
|
/>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="src/assets/img/logo.png" />
|
<link rel="shortcut icon" type="image/x-icon" href="src/assets/img/logo.png" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
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.
|
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:
|
Key integration points:
|
||||||
|
|
||||||
- **Existing Card System**: Devices must bind with cards from the existing card-list module
|
- **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
|
- **Shop System**: Devices are allocated to shops using the existing ShopService
|
||||||
- **Object Storage**: Imports use the existing StorageService for large file uploads
|
- **Object Storage**: Imports use the existing StorageService for large file uploads
|
||||||
@@ -25,16 +26,19 @@ Key integration points:
|
|||||||
**Choice**: Use StorageService.getUploadUrl → uploadFile → DeviceService.importDevices
|
**Choice**: Use StorageService.getUploadUrl → uploadFile → DeviceService.importDevices
|
||||||
|
|
||||||
**Rationale**:
|
**Rationale**:
|
||||||
|
|
||||||
- Handles large files (thousands of devices) without timeout issues
|
- Handles large files (thousands of devices) without timeout issues
|
||||||
- Decouples upload from processing (backend can process asynchronously)
|
- Decouples upload from processing (backend can process asynchronously)
|
||||||
- Consistent with modern cloud architecture patterns
|
- Consistent with modern cloud architecture patterns
|
||||||
- Allows progress tracking through task management
|
- Allows progress tracking through task management
|
||||||
|
|
||||||
**Alternatives Considered**:
|
**Alternatives Considered**:
|
||||||
|
|
||||||
- Direct multipart upload to backend (rejected: not scalable for large files)
|
- Direct multipart upload to backend (rejected: not scalable for large files)
|
||||||
- Two-step process without pre-signed URL (rejected: less secure, more backend load)
|
- Two-step process without pre-signed URL (rejected: less secure, more backend load)
|
||||||
|
|
||||||
**Implementation Notes**:
|
**Implementation Notes**:
|
||||||
|
|
||||||
- Frontend only handles upload to object storage, not file parsing
|
- Frontend only handles upload to object storage, not file parsing
|
||||||
- Backend processes the file asynchronously and creates task records
|
- Backend processes the file asynchronously and creates task records
|
||||||
- Task management provides visibility into import progress
|
- Task management provides visibility into import progress
|
||||||
@@ -44,16 +48,19 @@ Key integration points:
|
|||||||
**Choice**: Store binding with explicit slot_position (1-4) in device_cards table
|
**Choice**: Store binding with explicit slot_position (1-4) in device_cards table
|
||||||
|
|
||||||
**Rationale**:
|
**Rationale**:
|
||||||
|
|
||||||
- IoT devices have physical SIM slots that need explicit identification
|
- IoT devices have physical SIM slots that need explicit identification
|
||||||
- Each device can have 1-4 slots (max_sim_slots)
|
- Each device can have 1-4 slots (max_sim_slots)
|
||||||
- One card can only bind to one device slot (enforced by backend)
|
- One card can only bind to one device slot (enforced by backend)
|
||||||
- Slot position is critical for physical device configuration
|
- Slot position is critical for physical device configuration
|
||||||
|
|
||||||
**Alternatives Considered**:
|
**Alternatives Considered**:
|
||||||
|
|
||||||
- Auto-assign slot positions (rejected: operators need to know physical slot numbers)
|
- 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)
|
- Allow one card to bind to multiple devices (rejected: not realistic for physical SIMs)
|
||||||
|
|
||||||
**Implementation Notes**:
|
**Implementation Notes**:
|
||||||
|
|
||||||
- Device detail page shows a 4-slot grid (empty slots show "Bind Card" button)
|
- Device detail page shows a 4-slot grid (empty slots show "Bind Card" button)
|
||||||
- Binding dialog requires explicit slot selection
|
- Binding dialog requires explicit slot selection
|
||||||
- Unbinding updates bound_card_count and frees the slot
|
- Unbinding updates bound_card_count and frees the slot
|
||||||
@@ -63,16 +70,19 @@ Key integration points:
|
|||||||
**Choice**: Extend existing task-management to support device import tasks
|
**Choice**: Extend existing task-management to support device import tasks
|
||||||
|
|
||||||
**Rationale**:
|
**Rationale**:
|
||||||
|
|
||||||
- Reuses existing task infrastructure
|
- Reuses existing task infrastructure
|
||||||
- Provides consistent UX for all import operations
|
- Provides consistent UX for all import operations
|
||||||
- Avoids duplicate task tracking logic
|
- Avoids duplicate task tracking logic
|
||||||
- Allows unified search/filter across task types
|
- Allows unified search/filter across task types
|
||||||
|
|
||||||
**Alternatives Considered**:
|
**Alternatives Considered**:
|
||||||
|
|
||||||
- Separate device task management page (rejected: creates UX fragmentation)
|
- Separate device task management page (rejected: creates UX fragmentation)
|
||||||
- Embed task tracking only in device-import page (rejected: limited visibility)
|
- Embed task tracking only in device-import page (rejected: limited visibility)
|
||||||
|
|
||||||
**Implementation Notes**:
|
**Implementation Notes**:
|
||||||
|
|
||||||
- Add task_type field to distinguish ICCID vs Device imports
|
- Add task_type field to distinguish ICCID vs Device imports
|
||||||
- Task detail page renders different content based on task_type
|
- Task detail page renders different content based on task_type
|
||||||
- Device tasks show device_no and bound ICCIDs instead of just ICCID
|
- Device tasks show device_no and bound ICCIDs instead of just ICCID
|
||||||
@@ -82,35 +92,41 @@ Key integration points:
|
|||||||
**Choice**: Show detailed results in dialog after batch allocation/recall
|
**Choice**: Show detailed results in dialog after batch allocation/recall
|
||||||
|
|
||||||
**Rationale**:
|
**Rationale**:
|
||||||
|
|
||||||
- Operations may partially succeed (some devices succeed, others fail)
|
- Operations may partially succeed (some devices succeed, others fail)
|
||||||
- Operators need to know exactly which devices failed and why
|
- Operators need to know exactly which devices failed and why
|
||||||
- Allows retry of failed operations without re-selecting all devices
|
- Allows retry of failed operations without re-selecting all devices
|
||||||
|
|
||||||
**Alternatives Considered**:
|
**Alternatives Considered**:
|
||||||
|
|
||||||
- Just show toast notification (rejected: insufficient detail for partial failures)
|
- Just show toast notification (rejected: insufficient detail for partial failures)
|
||||||
- Navigate to separate results page (rejected: disrupts workflow)
|
- Navigate to separate results page (rejected: disrupts workflow)
|
||||||
|
|
||||||
**Implementation Notes**:
|
**Implementation Notes**:
|
||||||
|
|
||||||
- Dialog shows summary: total, success count, failure count
|
- Dialog shows summary: total, success count, failure count
|
||||||
- Expandable table shows failed devices with error messages
|
- Expandable table shows failed devices with error messages
|
||||||
- Success closes dialog, partial failure keeps it open for review
|
- Success closes dialog, partial failure keeps it open for review
|
||||||
|
|
||||||
### Decision 5: Component Reuse Strategy
|
### Decision 5: Component Reuse Strategy
|
||||||
|
|
||||||
**Choice**: Use existing Art* components (ArtTableFullScreen, ArtSearchBar, ArtTable, ArtButtonTable)
|
**Choice**: Use existing Art\* components (ArtTableFullScreen, ArtSearchBar, ArtTable, ArtButtonTable)
|
||||||
|
|
||||||
**Rationale**:
|
**Rationale**:
|
||||||
|
|
||||||
- Maintains UI consistency across the application
|
- Maintains UI consistency across the application
|
||||||
- Reduces development time
|
- Reduces development time
|
||||||
- Leverages tested, stable components
|
- Leverages tested, stable components
|
||||||
- Easier for users familiar with other pages
|
- Easier for users familiar with other pages
|
||||||
|
|
||||||
**Reference Implementation**:
|
**Reference Implementation**:
|
||||||
|
|
||||||
- Device-list follows role/index.vue pattern
|
- Device-list follows role/index.vue pattern
|
||||||
- Device-detail follows card-list detail modal pattern
|
- Device-detail follows card-list detail modal pattern
|
||||||
- Search form follows enterprise-customer search pattern
|
- Search form follows enterprise-customer search pattern
|
||||||
|
|
||||||
**Implementation Notes**:
|
**Implementation Notes**:
|
||||||
|
|
||||||
- Use CommonStatus for status values (ENABLED/DISABLED)
|
- Use CommonStatus for status values (ENABLED/DISABLED)
|
||||||
- Use ElSwitch for status toggles
|
- Use ElSwitch for status toggles
|
||||||
- Use ArtButtonTable for action buttons
|
- Use ArtButtonTable for action buttons
|
||||||
@@ -119,6 +135,7 @@ Key integration points:
|
|||||||
## Architecture Diagrams
|
## Architecture Diagrams
|
||||||
|
|
||||||
### Device Import Flow
|
### Device Import Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────┐ 1. Select CSV ┌──────────────┐
|
┌─────────┐ 1. Select CSV ┌──────────────┐
|
||||||
│ Admin │ ──────────────────> │ device-import│
|
│ Admin │ ──────────────────> │ device-import│
|
||||||
@@ -155,6 +172,7 @@ Key integration points:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Device-Card Binding
|
### Device-Card Binding
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────┐ ┌──────────────┐
|
┌─────────┐ ┌──────────────┐
|
||||||
│ Device │ ───── bound to ────> │ Card │
|
│ Device │ ───── bound to ────> │ Card │
|
||||||
@@ -175,12 +193,14 @@ Key integration points:
|
|||||||
## Data Flow
|
## Data Flow
|
||||||
|
|
||||||
### Device List Page Load
|
### Device List Page Load
|
||||||
|
|
||||||
1. User navigates to /asset-management/device-list
|
1. User navigates to /asset-management/device-list
|
||||||
2. Vue component mounts, calls DeviceService.getDevices(params)
|
2. Vue component mounts, calls DeviceService.getDevices(params)
|
||||||
3. Backend returns paginated device list with bound_card_count
|
3. Backend returns paginated device list with bound_card_count
|
||||||
4. Table renders with status switches and action buttons
|
4. Table renders with status switches and action buttons
|
||||||
|
|
||||||
### Batch Allocation Flow
|
### Batch Allocation Flow
|
||||||
|
|
||||||
1. User selects devices, clicks "Batch Allocate"
|
1. User selects devices, clicks "Batch Allocate"
|
||||||
2. Dialog opens with shop selector
|
2. Dialog opens with shop selector
|
||||||
3. User selects shop, adds remarks, confirms
|
3. User selects shop, adds remarks, confirms
|
||||||
@@ -189,6 +209,7 @@ Key integration points:
|
|||||||
6. Dialog shows summary and failed device details
|
6. Dialog shows summary and failed device details
|
||||||
|
|
||||||
### Card Binding Flow
|
### Card Binding Flow
|
||||||
|
|
||||||
1. User opens device detail page
|
1. User opens device detail page
|
||||||
2. Clicks "Bind Card" for an empty slot
|
2. Clicks "Bind Card" for an empty slot
|
||||||
3. Dialog shows card search and slot selection
|
3. Dialog shows card search and slot selection
|
||||||
@@ -202,11 +223,13 @@ Key integration points:
|
|||||||
### Updating Existing Device Import Page
|
### Updating Existing Device Import Page
|
||||||
|
|
||||||
**Current State** (`src/views/batch/device-import/index.vue`):
|
**Current State** (`src/views/batch/device-import/index.vue`):
|
||||||
|
|
||||||
- Uses ElUpload with drag-and-drop
|
- Uses ElUpload with drag-and-drop
|
||||||
- Mock data for import records
|
- Mock data for import records
|
||||||
- No real API integration
|
- No real API integration
|
||||||
|
|
||||||
**Migration Steps**:
|
**Migration Steps**:
|
||||||
|
|
||||||
1. Replace ElUpload with three-step upload logic
|
1. Replace ElUpload with three-step upload logic
|
||||||
- Add getUploadUrl call
|
- Add getUploadUrl call
|
||||||
- Add uploadFile to object storage
|
- Add uploadFile to object storage
|
||||||
@@ -216,6 +239,7 @@ Key integration points:
|
|||||||
4. Update CSV format instructions
|
4. Update CSV format instructions
|
||||||
|
|
||||||
**Backward Compatibility**:
|
**Backward Compatibility**:
|
||||||
|
|
||||||
- This is a new feature area with no existing production data
|
- This is a new feature area with no existing production data
|
||||||
- No API breaking changes
|
- No API breaking changes
|
||||||
- UI changes are additive (improve existing page)
|
- UI changes are additive (improve existing page)
|
||||||
@@ -223,16 +247,19 @@ Key integration points:
|
|||||||
### Extending Task Management
|
### Extending Task Management
|
||||||
|
|
||||||
**Current State**:
|
**Current State**:
|
||||||
|
|
||||||
- Only handles ICCID import tasks
|
- Only handles ICCID import tasks
|
||||||
- Single task type rendering
|
- Single task type rendering
|
||||||
|
|
||||||
**Migration Steps**:
|
**Migration Steps**:
|
||||||
|
|
||||||
1. Add task_type filter dropdown (default: show all)
|
1. Add task_type filter dropdown (default: show all)
|
||||||
2. Add task_type badge in task list
|
2. Add task_type badge in task list
|
||||||
3. Task detail page: switch rendering based on task_type
|
3. Task detail page: switch rendering based on task_type
|
||||||
4. Add device-specific fields to task detail view
|
4. Add device-specific fields to task detail view
|
||||||
|
|
||||||
**Backward Compatibility**:
|
**Backward Compatibility**:
|
||||||
|
|
||||||
- Existing ICCID tasks continue to work unchanged
|
- Existing ICCID tasks continue to work unchanged
|
||||||
- Filter defaults to showing all types
|
- Filter defaults to showing all types
|
||||||
- No database schema changes required (task_type already exists)
|
- No database schema changes required (task_type already exists)
|
||||||
@@ -240,23 +267,27 @@ Key integration points:
|
|||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
### Unit Testing
|
### Unit Testing
|
||||||
|
|
||||||
- DeviceService API methods with mock responses
|
- DeviceService API methods with mock responses
|
||||||
- Form validation logic
|
- Form validation logic
|
||||||
- Utility functions (formatters, validators)
|
- Utility functions (formatters, validators)
|
||||||
|
|
||||||
### Integration Testing
|
### Integration Testing
|
||||||
|
|
||||||
- Device list search and pagination
|
- Device list search and pagination
|
||||||
- Batch allocation with partial failures
|
- Batch allocation with partial failures
|
||||||
- Card binding/unbinding workflow
|
- Card binding/unbinding workflow
|
||||||
- Import task creation and status tracking
|
- Import task creation and status tracking
|
||||||
|
|
||||||
### E2E Testing Scenarios
|
### E2E Testing Scenarios
|
||||||
|
|
||||||
1. Import devices via CSV → verify task created → check task detail
|
1. Import devices via CSV → verify task created → check task detail
|
||||||
2. Search devices → select multiple → allocate to shop → verify shop assignment
|
2. Search devices → select multiple → allocate to shop → verify shop assignment
|
||||||
3. View device detail → bind card to slot 2 → unbind → verify empty slot
|
3. View device detail → bind card to slot 2 → unbind → verify empty slot
|
||||||
4. Batch recall devices → verify shop cleared → check operation history
|
4. Batch recall devices → verify shop cleared → check operation history
|
||||||
|
|
||||||
### Performance Considerations
|
### Performance Considerations
|
||||||
|
|
||||||
- Device list pagination (default 20 per page)
|
- Device list pagination (default 20 per page)
|
||||||
- Debounced search input (300ms delay)
|
- Debounced search input (300ms delay)
|
||||||
- Batch operation result pagination (if >100 results)
|
- Batch operation result pagination (if >100 results)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Why
|
## Why
|
||||||
|
|
||||||
当前系统只有设备导入页面,缺少完整的设备管理能力。需要提供设备的全生命周期管理,包括:
|
当前系统只有设备导入页面,缺少完整的设备管理能力。需要提供设备的全生命周期管理,包括:
|
||||||
|
|
||||||
- 查看和搜索设备列表
|
- 查看和搜索设备列表
|
||||||
- 查看设备详情和绑定的卡信息
|
- 查看设备详情和绑定的卡信息
|
||||||
- 管理设备与卡的绑定关系
|
- 管理设备与卡的绑定关系
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
## What Changes
|
## What Changes
|
||||||
|
|
||||||
### 新增功能
|
### 新增功能
|
||||||
|
|
||||||
- **设备列表页面**: 支持多条件搜索、分页、列筛选、批量操作(分配、回收、删除)
|
- **设备列表页面**: 支持多条件搜索、分页、列筛选、批量操作(分配、回收、删除)
|
||||||
- **设备详情页面**: 展示设备基本信息、绑定的卡列表、操作历史
|
- **设备详情页面**: 展示设备基本信息、绑定的卡列表、操作历史
|
||||||
- **卡绑定管理**: 在设备详情页绑定/解绑卡
|
- **卡绑定管理**: 在设备详情页绑定/解绑卡
|
||||||
@@ -23,10 +25,12 @@
|
|||||||
- **导入任务管理**: 任务列表和详情查看
|
- **导入任务管理**: 任务列表和详情查看
|
||||||
|
|
||||||
### API 集成
|
### API 集成
|
||||||
|
|
||||||
- DeviceService: 11个API接口
|
- DeviceService: 11个API接口
|
||||||
- 类型定义: Device, DeviceBinding, ImportTask 等
|
- 类型定义: Device, DeviceBinding, ImportTask 等
|
||||||
|
|
||||||
### UI 组件
|
### UI 组件
|
||||||
|
|
||||||
- 遵循现有组件模式 (ArtTableFullScreen, ArtSearchBar等)
|
- 遵循现有组件模式 (ArtTableFullScreen, ArtSearchBar等)
|
||||||
- 复用 CommonStatus 统一状态变量
|
- 复用 CommonStatus 统一状态变量
|
||||||
- 使用 ElDescriptions、ElTag、ElSwitch 等组件
|
- 使用 ElDescriptions、ElTag、ElSwitch 等组件
|
||||||
@@ -34,12 +38,14 @@
|
|||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
### 影响的功能模块
|
### 影响的功能模块
|
||||||
|
|
||||||
- **新增**: 资产管理 / 设备列表(主列表页)
|
- **新增**: 资产管理 / 设备列表(主列表页)
|
||||||
- **新增**: 资产管理 / 设备详情
|
- **新增**: 资产管理 / 设备详情
|
||||||
- **改进**: 批量操作 / 设备导入(改为对象存储模式)
|
- **改进**: 批量操作 / 设备导入(改为对象存储模式)
|
||||||
- **新增**: 批量操作 / 导入任务列表(独立页面)
|
- **新增**: 批量操作 / 导入任务列表(独立页面)
|
||||||
|
|
||||||
### 影响的代码
|
### 影响的代码
|
||||||
|
|
||||||
- `src/api/modules/device.ts` (新增)
|
- `src/api/modules/device.ts` (新增)
|
||||||
- `src/types/api/device.ts` (新增)
|
- `src/types/api/device.ts` (新增)
|
||||||
- `src/views/asset-management/device-list/index.vue` (新增)
|
- `src/views/asset-management/device-list/index.vue` (新增)
|
||||||
@@ -54,6 +60,7 @@
|
|||||||
- `src/types/api/index.ts` (导出 Device 类型)
|
- `src/types/api/index.ts` (导出 Device 类型)
|
||||||
|
|
||||||
### 依赖关系
|
### 依赖关系
|
||||||
|
|
||||||
- 依赖现有的 StorageService (对象存储)
|
- 依赖现有的 StorageService (对象存储)
|
||||||
- 依赖现有的 ShopService (店铺选择)
|
- 依赖现有的 ShopService (店铺选择)
|
||||||
- 设备与卡的关联管理
|
- 设备与卡的关联管理
|
||||||
|
|||||||
@@ -7,140 +7,140 @@
|
|||||||
The system SHALL provide a searchable device list with multi-condition filtering, pagination, and batch operations.
|
The system SHALL provide a searchable device list with multi-condition filtering, pagination, and batch operations.
|
||||||
|
|
||||||
#### Scenario: Search devices by multiple criteria
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
### Requirement: Device Detail Viewing
|
||||||
|
|
||||||
The system SHALL display comprehensive device information including basic properties and bound SIM cards.
|
The system SHALL display comprehensive device information including basic properties and bound SIM cards.
|
||||||
|
|
||||||
#### Scenario: View device basic information
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
### Requirement: Device-Card Binding Management
|
||||||
|
|
||||||
The system SHALL allow administrators to bind and unbind SIM cards to specific device slots.
|
The system SHALL allow administrators to bind and unbind SIM cards to specific device slots.
|
||||||
|
|
||||||
#### Scenario: Bind a card to a device slot
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
### Requirement: Batch Device Allocation
|
||||||
|
|
||||||
The system SHALL support batch allocation of devices to shops with result tracking.
|
The system SHALL support batch allocation of devices to shops with result tracking.
|
||||||
|
|
||||||
#### Scenario: Allocate multiple devices to a shop
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
### Requirement: Batch Device Recall
|
||||||
|
|
||||||
The system SHALL support batch recall of devices from shops with result tracking.
|
The system SHALL support batch recall of devices from shops with result tracking.
|
||||||
|
|
||||||
#### Scenario: Recall multiple devices
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
### Requirement: Device Import via Object Storage
|
||||||
|
|
||||||
The system SHALL support CSV-based device import using a three-step object storage upload process.
|
The system SHALL support CSV-based device import using a three-step object storage upload process.
|
||||||
|
|
||||||
#### Scenario: Import devices with three-step process
|
#### Scenario: Import devices with three-step process
|
||||||
**WHEN** an administrator uploads a CSV file
|
|
||||||
**THEN** the system shall:
|
**WHEN** an administrator uploads a CSV file **THEN** the system shall:
|
||||||
|
|
||||||
1. Get upload URL from StorageService.getUploadUrl
|
1. Get upload URL from StorageService.getUploadUrl
|
||||||
2. Upload file to object storage using StorageService.uploadFile
|
2. Upload file to object storage using StorageService.uploadFile
|
||||||
3. Call DeviceService.importDevices with the file_key
|
3. Call DeviceService.importDevices with the file_key **AND** display the task number for tracking
|
||||||
**AND** display the task number for tracking
|
|
||||||
|
|
||||||
#### Scenario: Validate CSV format
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
### Requirement: Import Task Management
|
||||||
|
|
||||||
The system SHALL track device import tasks with detailed status and record information.
|
The system SHALL track device import tasks with detailed status and record information.
|
||||||
|
|
||||||
#### Scenario: List import tasks with type filter
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**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
|
#### 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
|
**WHEN** a device import completes successfully **THEN** the system shall display the task number with a link to view task details
|
||||||
|
|
||||||
## MODIFIED Requirements
|
## MODIFIED Requirements
|
||||||
|
|
||||||
@@ -151,9 +151,9 @@ Task management SHALL support multiple task types including ICCID import and Dev
|
|||||||
**Previous behavior**: Task management only supported ICCID import tasks.
|
**Previous behavior**: Task management only supported ICCID import tasks.
|
||||||
|
|
||||||
#### Scenario: Filter tasks by type
|
#### 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
|
**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
|
#### 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
|
**WHEN** viewing the task list **THEN** each task shall display a type badge indicating whether it's an ICCID import or Device import task
|
||||||
|
|||||||
@@ -15,18 +15,21 @@
|
|||||||
新增企业设备授权管理功能,包括:
|
新增企业设备授权管理功能,包括:
|
||||||
|
|
||||||
### 类型定义
|
### 类型定义
|
||||||
|
|
||||||
- 新增 `src/types/api/enterpriseDevice.ts` 文件
|
- 新增 `src/types/api/enterpriseDevice.ts` 文件
|
||||||
- 定义设备列表项、查询参数、分页结果类型
|
- 定义设备列表项、查询参数、分页结果类型
|
||||||
- 定义授权/撤销请求和响应类型
|
- 定义授权/撤销请求和响应类型
|
||||||
- 在 `src/types/api/index.ts` 中导出新类型
|
- 在 `src/types/api/index.ts` 中导出新类型
|
||||||
|
|
||||||
### API 服务层
|
### API 服务层
|
||||||
|
|
||||||
- 扩展 `EnterpriseService` 类,新增3个方法:
|
- 扩展 `EnterpriseService` 类,新增3个方法:
|
||||||
- `allocateDevices(enterpriseId, data)` - POST 授权设备
|
- `allocateDevices(enterpriseId, data)` - POST 授权设备
|
||||||
- `getEnterpriseDevices(enterpriseId, params)` - GET 设备列表
|
- `getEnterpriseDevices(enterpriseId, params)` - GET 设备列表
|
||||||
- `recallDevices(enterpriseId, data)` - POST 撤销授权
|
- `recallDevices(enterpriseId, data)` - POST 撤销授权
|
||||||
|
|
||||||
### 视图层
|
### 视图层
|
||||||
|
|
||||||
- 新增 `src/views/asset-management/enterprise-devices/index.vue` 页面
|
- 新增 `src/views/asset-management/enterprise-devices/index.vue` 页面
|
||||||
- 实现设备列表展示 (表格、分页、搜索)
|
- 实现设备列表展示 (表格、分页、搜索)
|
||||||
- 实现授权设备对话框 (支持批量输入设备号)
|
- 实现授权设备对话框 (支持批量输入设备号)
|
||||||
@@ -34,10 +37,12 @@
|
|||||||
- 实现操作结果展示 (成功/失败统计)
|
- 实现操作结果展示 (成功/失败统计)
|
||||||
|
|
||||||
### 路由配置
|
### 路由配置
|
||||||
|
|
||||||
- 在 `src/router/routesAlias.ts` 添加路由别名
|
- 在 `src/router/routesAlias.ts` 添加路由别名
|
||||||
- 在 `src/router/routes/asyncRoutes.ts` 的资产管理模块下添加子路由
|
- 在 `src/router/routes/asyncRoutes.ts` 的资产管理模块下添加子路由
|
||||||
|
|
||||||
### 国际化
|
### 国际化
|
||||||
|
|
||||||
- 在 `src/locales/langs/zh.json` 和 `en.json` 添加中英文翻译
|
- 在 `src/locales/langs/zh.json` 和 `en.json` 添加中英文翻译
|
||||||
- 包含菜单、表单、表格、对话框、提示消息等所有文案
|
- 包含菜单、表单、表格、对话框、提示消息等所有文案
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,8 @@
|
|||||||
|
|
||||||
#### Scenario: 定义企业设备列表项类型
|
#### Scenario: 定义企业设备列表项类型
|
||||||
|
|
||||||
**Given** 需要展示企业设备列表
|
**Given** 需要展示企业设备列表 **When** 定义 `EnterpriseDeviceItem` 接口 **Then** 接口必须包含以下字段:
|
||||||
**When** 定义 `EnterpriseDeviceItem` 接口
|
|
||||||
**Then** 接口必须包含以下字段:
|
|
||||||
- `device_id: number` - 设备ID
|
- `device_id: number` - 设备ID
|
||||||
- `device_no: string` - 设备号
|
- `device_no: string` - 设备号
|
||||||
- `device_name: string` - 设备名称
|
- `device_name: string` - 设备名称
|
||||||
@@ -24,52 +23,49 @@
|
|||||||
|
|
||||||
#### Scenario: 定义设备列表查询参数
|
#### Scenario: 定义设备列表查询参数
|
||||||
|
|
||||||
**Given** 需要查询和搜索企业设备
|
**Given** 需要查询和搜索企业设备 **When** 定义 `EnterpriseDeviceListParams` 接口 **Then** 接口必须包含以下可选字段:
|
||||||
**When** 定义 `EnterpriseDeviceListParams` 接口
|
|
||||||
**Then** 接口必须包含以下可选字段:
|
|
||||||
- `page?: number` - 页码
|
- `page?: number` - 页码
|
||||||
- `page_size?: number` - 每页数量
|
- `page_size?: number` - 每页数量
|
||||||
- `device_no?: string` - 设备号模糊搜索
|
- `device_no?: string` - 设备号模糊搜索
|
||||||
|
|
||||||
#### Scenario: 定义授权设备请求类型
|
#### Scenario: 定义授权设备请求类型
|
||||||
|
|
||||||
**Given** 需要授权设备给企业
|
**Given** 需要授权设备给企业 **When** 定义 `AllocateDevicesRequest` 接口 **Then** 接口必须包含:
|
||||||
**When** 定义 `AllocateDevicesRequest` 接口
|
|
||||||
**Then** 接口必须包含:
|
|
||||||
- `device_nos: string[]` - 设备号列表 (nullable, 最多100个)
|
- `device_nos: string[]` - 设备号列表 (nullable, 最多100个)
|
||||||
- `remark?: string` - 授权备注
|
- `remark?: string` - 授权备注
|
||||||
|
|
||||||
#### Scenario: 定义授权设备响应类型
|
#### Scenario: 定义授权设备响应类型
|
||||||
|
|
||||||
**Given** 授权操作需要返回详细结果
|
**Given** 授权操作需要返回详细结果 **When** 定义 `AllocateDevicesResponse` 接口 **Then** 接口必须包含:
|
||||||
**When** 定义 `AllocateDevicesResponse` 接口
|
|
||||||
**Then** 接口必须包含:
|
|
||||||
- `success_count: number` - 成功数量
|
- `success_count: number` - 成功数量
|
||||||
- `fail_count: number` - 失败数量
|
- `fail_count: number` - 失败数量
|
||||||
- `authorized_devices: AuthorizedDeviceItem[]` - 已授权设备列表 (nullable)
|
- `authorized_devices: AuthorizedDeviceItem[]` - 已授权设备列表 (nullable)
|
||||||
- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable)
|
- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable)
|
||||||
|
|
||||||
**And** `AuthorizedDeviceItem` 包含:
|
**And** `AuthorizedDeviceItem` 包含:
|
||||||
|
|
||||||
- `device_id: number` - 设备ID
|
- `device_id: number` - 设备ID
|
||||||
- `device_no: string` - 设备号
|
- `device_no: string` - 设备号
|
||||||
- `card_count: number` - 绑定卡数量
|
- `card_count: number` - 绑定卡数量
|
||||||
|
|
||||||
**And** `FailedDeviceItem` 包含:
|
**And** `FailedDeviceItem` 包含:
|
||||||
|
|
||||||
- `device_no: string` - 设备号
|
- `device_no: string` - 设备号
|
||||||
- `reason: string` - 失败原因
|
- `reason: string` - 失败原因
|
||||||
|
|
||||||
#### Scenario: 定义撤销授权请求类型
|
#### Scenario: 定义撤销授权请求类型
|
||||||
|
|
||||||
**Given** 需要撤销设备授权
|
**Given** 需要撤销设备授权 **When** 定义 `RecallDevicesRequest` 接口 **Then** 接口必须包含:
|
||||||
**When** 定义 `RecallDevicesRequest` 接口
|
|
||||||
**Then** 接口必须包含:
|
|
||||||
- `device_nos: string[]` - 设备号列表 (nullable, 最多100个)
|
- `device_nos: string[]` - 设备号列表 (nullable, 最多100个)
|
||||||
|
|
||||||
#### Scenario: 定义撤销授权响应类型
|
#### Scenario: 定义撤销授权响应类型
|
||||||
|
|
||||||
**Given** 撤销操作需要返回结果统计
|
**Given** 撤销操作需要返回结果统计 **When** 定义 `RecallDevicesResponse` 接口 **Then** 接口必须包含:
|
||||||
**When** 定义 `RecallDevicesResponse` 接口
|
|
||||||
**Then** 接口必须包含:
|
|
||||||
- `success_count: number` - 成功数量
|
- `success_count: number` - 成功数量
|
||||||
- `fail_count: number` - 失败数量
|
- `fail_count: number` - 失败数量
|
||||||
- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable)
|
- `failed_items: FailedDeviceItem[]` - 失败项列表 (nullable)
|
||||||
@@ -82,28 +78,15 @@
|
|||||||
|
|
||||||
#### Scenario: 授权设备给企业
|
#### Scenario: 授权设备给企业
|
||||||
|
|
||||||
**Given** 运营人员需要授权设备给企业客户
|
**Given** 运营人员需要授权设备给企业客户 **When** 调用 `EnterpriseService.allocateDevices(enterpriseId, data)` **Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/allocate-devices` **And** 请求体必须包含设备号列表和可选备注 **And** 返回授权结果,包含成功/失败统计和详细列表
|
||||||
**When** 调用 `EnterpriseService.allocateDevices(enterpriseId, data)`
|
|
||||||
**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/allocate-devices`
|
|
||||||
**And** 请求体必须包含设备号列表和可选备注
|
|
||||||
**And** 返回授权结果,包含成功/失败统计和详细列表
|
|
||||||
|
|
||||||
#### Scenario: 获取企业设备列表
|
#### Scenario: 获取企业设备列表
|
||||||
|
|
||||||
**Given** 需要查看企业的设备列表
|
**Given** 需要查看企业的设备列表 **When** 调用 `EnterpriseService.getEnterpriseDevices(enterpriseId, params)` **Then** 必须发送 GET 请求到 `/api/admin/enterprises/{id}/devices` **And** 支持分页参数 (page, page_size) **And** 支持设备号模糊搜索 **And** 返回设备列表和总数
|
||||||
**When** 调用 `EnterpriseService.getEnterpriseDevices(enterpriseId, params)`
|
|
||||||
**Then** 必须发送 GET 请求到 `/api/admin/enterprises/{id}/devices`
|
|
||||||
**And** 支持分页参数 (page, page_size)
|
|
||||||
**And** 支持设备号模糊搜索
|
|
||||||
**And** 返回设备列表和总数
|
|
||||||
|
|
||||||
#### Scenario: 撤销设备授权
|
#### Scenario: 撤销设备授权
|
||||||
|
|
||||||
**Given** 需要撤销企业的设备授权
|
**Given** 需要撤销企业的设备授权 **When** 调用 `EnterpriseService.recallDevices(enterpriseId, data)` **Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/recall-devices` **And** 请求体必须包含设备号列表 **And** 返回撤销结果,包含成功/失败统计和失败原因
|
||||||
**When** 调用 `EnterpriseService.recallDevices(enterpriseId, data)`
|
|
||||||
**Then** 必须发送 POST 请求到 `/api/admin/enterprises/{id}/recall-devices`
|
|
||||||
**And** 请求体必须包含设备号列表
|
|
||||||
**And** 返回撤销结果,包含成功/失败统计和失败原因
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -113,10 +96,8 @@
|
|||||||
|
|
||||||
#### Scenario: 显示企业设备列表
|
#### Scenario: 显示企业设备列表
|
||||||
|
|
||||||
**Given** 用户访问企业设备列表页面
|
**Given** 用户访问企业设备列表页面 **When** 页面加载完成 **Then** 必须显示设备列表表格 **And** 表格必须包含以下列:
|
||||||
**When** 页面加载完成
|
|
||||||
**Then** 必须显示设备列表表格
|
|
||||||
**And** 表格必须包含以下列:
|
|
||||||
- 设备ID
|
- 设备ID
|
||||||
- 设备号
|
- 设备号
|
||||||
- 设备名称
|
- 设备名称
|
||||||
@@ -124,56 +105,32 @@
|
|||||||
- 绑定卡数量
|
- 绑定卡数量
|
||||||
- 授权时间
|
- 授权时间
|
||||||
|
|
||||||
**And** 必须支持分页功能
|
**And** 必须支持分页功能 **And** 必须显示加载状态
|
||||||
**And** 必须显示加载状态
|
|
||||||
|
|
||||||
#### Scenario: 搜索企业设备
|
#### Scenario: 搜索企业设备
|
||||||
|
|
||||||
**Given** 设备列表已加载
|
**Given** 设备列表已加载 **When** 用户在搜索框输入设备号 **And** 点击搜索按钮 **Then** 必须根据设备号模糊查询设备 **And** 必须更新设备列表显示 **And** 必须重置到第一页
|
||||||
**When** 用户在搜索框输入设备号
|
|
||||||
**And** 点击搜索按钮
|
|
||||||
**Then** 必须根据设备号模糊查询设备
|
|
||||||
**And** 必须更新设备列表显示
|
|
||||||
**And** 必须重置到第一页
|
|
||||||
|
|
||||||
#### Scenario: 授权设备对话框
|
#### Scenario: 授权设备对话框
|
||||||
|
|
||||||
**Given** 用户点击"授权设备"按钮
|
**Given** 用户点击"授权设备"按钮 **When** 授权设备对话框打开 **Then** 必须显示设备号输入框 (textarea) **And** 必须显示备注输入框 (可选) **And** 必须提示支持的输入格式 (换行或逗号分隔) **And** 必须提示最多100个设备号限制 **And** 必须有表单验证 (设备号必填)
|
||||||
**When** 授权设备对话框打开
|
|
||||||
**Then** 必须显示设备号输入框 (textarea)
|
|
||||||
**And** 必须显示备注输入框 (可选)
|
|
||||||
**And** 必须提示支持的输入格式 (换行或逗号分隔)
|
|
||||||
**And** 必须提示最多100个设备号限制
|
|
||||||
**And** 必须有表单验证 (设备号必填)
|
|
||||||
|
|
||||||
#### Scenario: 提交授权设备
|
#### Scenario: 提交授权设备
|
||||||
|
|
||||||
**Given** 用户在对话框中输入了设备号列表
|
**Given** 用户在对话框中输入了设备号列表 **When** 用户点击提交按钮 **Then** 必须解析设备号列表 (支持换行和逗号分隔) **And** 必须去除空白字符和空行 **And** 必须验证设备号数量不超过100个 **And** 必须调用授权 API **And** 必须显示加载状态 **And** 授权完成后必须展示结果:
|
||||||
**When** 用户点击提交按钮
|
|
||||||
**Then** 必须解析设备号列表 (支持换行和逗号分隔)
|
|
||||||
**And** 必须去除空白字符和空行
|
|
||||||
**And** 必须验证设备号数量不超过100个
|
|
||||||
**And** 必须调用授权 API
|
|
||||||
**And** 必须显示加载状态
|
|
||||||
**And** 授权完成后必须展示结果:
|
|
||||||
- 成功数量
|
- 成功数量
|
||||||
- 失败数量
|
- 失败数量
|
||||||
- 失败设备列表及原因
|
- 失败设备列表及原因
|
||||||
|
|
||||||
**And** 如果有成功授权的设备,必须刷新设备列表
|
**And** 如果有成功授权的设备,必须刷新设备列表 **And** 必须关闭对话框
|
||||||
**And** 必须关闭对话框
|
|
||||||
|
|
||||||
#### Scenario: 撤销设备授权
|
#### Scenario: 撤销设备授权
|
||||||
|
|
||||||
**Given** 用户选中了要撤销的设备
|
**Given** 用户选中了要撤销的设备 **When** 用户点击"撤销授权"按钮 **Then** 必须显示二次确认对话框 **And** 确认对话框必须显示将要撤销的设备数量
|
||||||
**When** 用户点击"撤销授权"按钮
|
|
||||||
**Then** 必须显示二次确认对话框
|
**When** 用户确认撤销 **Then** 必须调用撤销 API **And** 必须显示加载状态 **And** 撤销完成后必须展示结果:
|
||||||
**And** 确认对话框必须显示将要撤销的设备数量
|
|
||||||
|
|
||||||
**When** 用户确认撤销
|
|
||||||
**Then** 必须调用撤销 API
|
|
||||||
**And** 必须显示加载状态
|
|
||||||
**And** 撤销完成后必须展示结果:
|
|
||||||
- 成功数量
|
- 成功数量
|
||||||
- 失败数量
|
- 失败数量
|
||||||
- 失败设备列表及原因
|
- 失败设备列表及原因
|
||||||
@@ -182,19 +139,11 @@
|
|||||||
|
|
||||||
#### Scenario: 错误处理
|
#### Scenario: 错误处理
|
||||||
|
|
||||||
**Given** API 调用可能失败
|
**Given** API 调用可能失败 **When** API 返回错误 **Then** 必须显示友好的错误提示消息 **And** 必须在控制台记录错误详情 **And** 必须停止加载状态
|
||||||
**When** API 返回错误
|
|
||||||
**Then** 必须显示友好的错误提示消息
|
|
||||||
**And** 必须在控制台记录错误详情
|
|
||||||
**And** 必须停止加载状态
|
|
||||||
|
|
||||||
#### Scenario: 分页切换
|
#### Scenario: 分页切换
|
||||||
|
|
||||||
**Given** 设备列表超过一页
|
**Given** 设备列表超过一页 **When** 用户切换页码或每页数量 **Then** 必须保持当前的搜索条件 **And** 必须重新加载设备列表 **And** 必须显示加载状态
|
||||||
**When** 用户切换页码或每页数量
|
|
||||||
**Then** 必须保持当前的搜索条件
|
|
||||||
**And** 必须重新加载设备列表
|
|
||||||
**And** 必须显示加载状态
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -204,13 +153,8 @@
|
|||||||
|
|
||||||
#### Scenario: 注册企业设备列表路由
|
#### Scenario: 注册企业设备列表路由
|
||||||
|
|
||||||
**Given** 需要访问企业设备列表页面
|
**Given** 需要访问企业设备列表页面 **When** 配置路由 **Then** 必须在资产管理模块 (`/asset-management`) 下添加子路由 **And** 路由路径必须为 `enterprise-devices` **And** 路由名称必须为 `EnterpriseDevices` **And** 必须使用路由别名 `RoutesAlias.EnterpriseDevices` **And** 必须配置 meta 信息:
|
||||||
**When** 配置路由
|
|
||||||
**Then** 必须在资产管理模块 (`/asset-management`) 下添加子路由
|
|
||||||
**And** 路由路径必须为 `enterprise-devices`
|
|
||||||
**And** 路由名称必须为 `EnterpriseDevices`
|
|
||||||
**And** 必须使用路由别名 `RoutesAlias.EnterpriseDevices`
|
|
||||||
**And** 必须配置 meta 信息:
|
|
||||||
- `title: 'menus.assetManagement.enterpriseDevices'`
|
- `title: 'menus.assetManagement.enterpriseDevices'`
|
||||||
- `keepAlive: true`
|
- `keepAlive: true`
|
||||||
|
|
||||||
@@ -222,9 +166,8 @@
|
|||||||
|
|
||||||
#### Scenario: 中文翻译
|
#### Scenario: 中文翻译
|
||||||
|
|
||||||
**Given** 系统语言设置为中文
|
**Given** 系统语言设置为中文 **When** 访问企业设备相关页面 **Then** 所有文本必须显示中文,包括:
|
||||||
**When** 访问企业设备相关页面
|
|
||||||
**Then** 所有文本必须显示中文,包括:
|
|
||||||
- 菜单标题: "企业设备列表"
|
- 菜单标题: "企业设备列表"
|
||||||
- 搜索表单标签和占位符
|
- 搜索表单标签和占位符
|
||||||
- 表格列名
|
- 表格列名
|
||||||
@@ -234,9 +177,8 @@
|
|||||||
|
|
||||||
#### Scenario: 英文翻译
|
#### Scenario: 英文翻译
|
||||||
|
|
||||||
**Given** 系统语言设置为英文
|
**Given** 系统语言设置为英文 **When** 访问企业设备相关页面 **Then** 所有文本必须显示英文,包括:
|
||||||
**When** 访问企业设备相关页面
|
|
||||||
**Then** 所有文本必须显示英文,包括:
|
|
||||||
- 菜单标题: "Enterprise Devices"
|
- 菜单标题: "Enterprise Devices"
|
||||||
- 搜索表单标签和占位符
|
- 搜索表单标签和占位符
|
||||||
- 表格列名
|
- 表格列名
|
||||||
@@ -252,37 +194,28 @@
|
|||||||
|
|
||||||
#### Scenario: 批量输入设备号
|
#### Scenario: 批量输入设备号
|
||||||
|
|
||||||
**Given** 用户需要授权多个设备
|
**Given** 用户需要授权多个设备 **When** 在设备号输入框中输入 **Then** 必须支持以下输入方式:
|
||||||
**When** 在设备号输入框中输入
|
|
||||||
**Then** 必须支持以下输入方式:
|
|
||||||
- 每行一个设备号
|
- 每行一个设备号
|
||||||
- 逗号分隔的设备号
|
- 逗号分隔的设备号
|
||||||
- 混合使用换行和逗号
|
- 混合使用换行和逗号
|
||||||
|
|
||||||
**And** 系统必须能正确解析所有格式
|
**And** 系统必须能正确解析所有格式 **And** 必须自动去除首尾空白字符 **And** 必须过滤空行
|
||||||
**And** 必须自动去除首尾空白字符
|
|
||||||
**And** 必须过滤空行
|
|
||||||
|
|
||||||
#### Scenario: 操作结果展示
|
#### Scenario: 操作结果展示
|
||||||
|
|
||||||
**Given** 批量操作完成
|
**Given** 批量操作完成 **When** 显示操作结果 **Then** 必须清晰展示:
|
||||||
**When** 显示操作结果
|
|
||||||
**Then** 必须清晰展示:
|
|
||||||
- 总共处理的数量
|
- 总共处理的数量
|
||||||
- 成功的数量
|
- 成功的数量
|
||||||
- 失败的数量
|
- 失败的数量
|
||||||
- 每个失败项的设备号和失败原因
|
- 每个失败项的设备号和失败原因
|
||||||
|
|
||||||
**And** 如果全部成功,必须显示成功提示
|
**And** 如果全部成功,必须显示成功提示 **And** 如果部分失败,必须显示警告提示 **And** 如果全部失败,必须显示错误提示
|
||||||
**And** 如果部分失败,必须显示警告提示
|
|
||||||
**And** 如果全部失败,必须显示错误提示
|
|
||||||
|
|
||||||
#### Scenario: 表格列管理
|
#### Scenario: 表格列管理
|
||||||
|
|
||||||
**Given** 设备列表表格已显示
|
**Given** 设备列表表格已显示 **When** 用户点击列管理按钮 **Then** 必须能够选择显示/隐藏的列 **And** 列配置必须被保存
|
||||||
**When** 用户点击列管理按钮
|
|
||||||
**Then** 必须能够选择显示/隐藏的列
|
|
||||||
**And** 列配置必须被保存
|
|
||||||
|
|
||||||
## Related Specs
|
## Related Specs
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
### Phase 1: Type Definitions (Foundation)
|
### Phase 1: Type Definitions (Foundation)
|
||||||
|
|
||||||
1. **创建企业设备类型定义文件**
|
1. **创建企业设备类型定义文件**
|
||||||
|
|
||||||
- 创建 `src/types/api/enterpriseDevice.ts`
|
- 创建 `src/types/api/enterpriseDevice.ts`
|
||||||
- 定义 `EnterpriseDeviceItem` 接口 (设备列表项)
|
- 定义 `EnterpriseDeviceItem` 接口 (设备列表项)
|
||||||
- 定义 `EnterpriseDeviceListParams` 接口 (查询参数)
|
- 定义 `EnterpriseDeviceListParams` 接口 (查询参数)
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
### Phase 3: Internationalization
|
### Phase 3: Internationalization
|
||||||
|
|
||||||
4. **添加中文翻译**
|
4. **添加中文翻译**
|
||||||
|
|
||||||
- 在 `src/locales/langs/zh.json` 的 `menus.assetManagement` 下添加 `enterpriseDevices` 条目
|
- 在 `src/locales/langs/zh.json` 的 `menus.assetManagement` 下添加 `enterpriseDevices` 条目
|
||||||
- 在 `src/locales/langs/zh.json` 添加 `enterpriseDevices` 模块的所有中文文案:
|
- 在 `src/locales/langs/zh.json` 添加 `enterpriseDevices` 模块的所有中文文案:
|
||||||
- 页面标题和搜索表单
|
- 页面标题和搜索表单
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
### Phase 4: Routing
|
### Phase 4: Routing
|
||||||
|
|
||||||
6. **添加路由别名**
|
6. **添加路由别名**
|
||||||
|
|
||||||
- 在 `src/router/routesAlias.ts` 添加 `EnterpriseDevices = '/asset-management/enterprise-devices'`
|
- 在 `src/router/routesAlias.ts` 添加 `EnterpriseDevices = '/asset-management/enterprise-devices'`
|
||||||
- **验证**: 确认导出正确
|
- **验证**: 确认导出正确
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@
|
|||||||
### Phase 5: UI Components
|
### Phase 5: UI Components
|
||||||
|
|
||||||
8. **创建企业设备列表页面**
|
8. **创建企业设备列表页面**
|
||||||
|
|
||||||
- 创建 `src/views/asset-management/enterprise-devices/index.vue`
|
- 创建 `src/views/asset-management/enterprise-devices/index.vue`
|
||||||
- 实现页面基础结构:
|
- 实现页面基础结构:
|
||||||
- 使用 `ArtTableFullScreen` 布局
|
- 使用 `ArtTableFullScreen` 布局
|
||||||
@@ -75,6 +79,7 @@
|
|||||||
- **验证**: 页面能正常渲染,无控制台错误
|
- **验证**: 页面能正常渲染,无控制台错误
|
||||||
|
|
||||||
9. **实现设备列表查询功能**
|
9. **实现设备列表查询功能**
|
||||||
|
|
||||||
- 实现 `loadDeviceList()` 方法调用 API
|
- 实现 `loadDeviceList()` 方法调用 API
|
||||||
- 实现搜索和重置功能
|
- 实现搜索和重置功能
|
||||||
- 实现分页功能
|
- 实现分页功能
|
||||||
@@ -82,6 +87,7 @@
|
|||||||
- **验证**: 能正确展示设备列表数据,分页工作正常
|
- **验证**: 能正确展示设备列表数据,分页工作正常
|
||||||
|
|
||||||
10. **实现授权设备对话框**
|
10. **实现授权设备对话框**
|
||||||
|
|
||||||
- 创建授权设备对话框
|
- 创建授权设备对话框
|
||||||
- 使用 `ElForm` + `ElInput` (textarea) 输入设备号列表
|
- 使用 `ElForm` + `ElInput` (textarea) 输入设备号列表
|
||||||
- 支持多行输入或逗号分隔
|
- 支持多行输入或逗号分隔
|
||||||
@@ -90,6 +96,7 @@
|
|||||||
- **验证**: 对话框显示正常,表单验证工作
|
- **验证**: 对话框显示正常,表单验证工作
|
||||||
|
|
||||||
11. **实现授权设备提交逻辑**
|
11. **实现授权设备提交逻辑**
|
||||||
|
|
||||||
- 实现 `handleAllocateDevices()` 方法
|
- 实现 `handleAllocateDevices()` 方法
|
||||||
- 解析设备号列表 (处理换行和逗号分隔)
|
- 解析设备号列表 (处理换行和逗号分隔)
|
||||||
- 调用 `EnterpriseService.allocateDevices()` API
|
- 调用 `EnterpriseService.allocateDevices()` API
|
||||||
@@ -99,6 +106,7 @@
|
|||||||
- **验证**: 能成功授权设备,正确处理部分成功/失败情况
|
- **验证**: 能成功授权设备,正确处理部分成功/失败情况
|
||||||
|
|
||||||
12. **实现撤销授权对话框**
|
12. **实现撤销授权对话框**
|
||||||
|
|
||||||
- 创建撤销授权对话框
|
- 创建撤销授权对话框
|
||||||
- 使用表格多选模式选择要撤销的设备
|
- 使用表格多选模式选择要撤销的设备
|
||||||
- 或者使用输入框输入设备号列表
|
- 或者使用输入框输入设备号列表
|
||||||
@@ -116,18 +124,21 @@
|
|||||||
### Phase 6: Polish & Testing
|
### Phase 6: Polish & Testing
|
||||||
|
|
||||||
14. **完善表格列配置**
|
14. **完善表格列配置**
|
||||||
|
|
||||||
- 配置表格列 (设备ID,设备号,设备名称,设备型号,绑定卡数量,授权时间)
|
- 配置表格列 (设备ID,设备号,设备名称,设备型号,绑定卡数量,授权时间)
|
||||||
- 实现列显示/隐藏功能
|
- 实现列显示/隐藏功能
|
||||||
- 添加时间格式化
|
- 添加时间格式化
|
||||||
- **验证**: 表格数据展示完整美观
|
- **验证**: 表格数据展示完整美观
|
||||||
|
|
||||||
15. **添加错误处理**
|
15. **添加错误处理**
|
||||||
|
|
||||||
- 为所有 API 调用添加 try-catch
|
- 为所有 API 调用添加 try-catch
|
||||||
- 添加友好的错误提示消息
|
- 添加友好的错误提示消息
|
||||||
- 处理网络错误和业务错误
|
- 处理网络错误和业务错误
|
||||||
- **验证**: 各种错误场景都有适当提示
|
- **验证**: 各种错误场景都有适当提示
|
||||||
|
|
||||||
16. **样式调整**
|
16. **样式调整**
|
||||||
|
|
||||||
- 确保页面样式与系统其他页面一致
|
- 确保页面样式与系统其他页面一致
|
||||||
- 响应式布局适配
|
- 响应式布局适配
|
||||||
- 对话框尺寸和布局优化
|
- 对话框尺寸和布局优化
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Why
|
## Why
|
||||||
|
|
||||||
The IoT management platform currently lacks order management capabilities. Users need to:
|
The IoT management platform currently lacks order management capabilities. Users need to:
|
||||||
|
|
||||||
- View and query orders created by customers (personal and agent)
|
- View and query orders created by customers (personal and agent)
|
||||||
- Track order payment status and details
|
- Track order payment status and details
|
||||||
- Create orders for single card or device purchases
|
- Create orders for single card or device purchases
|
||||||
@@ -20,6 +21,7 @@ This capability is essential for financial tracking, commission calculation, and
|
|||||||
- **NEW**: Router configuration for order management module
|
- **NEW**: Router configuration for order management module
|
||||||
|
|
||||||
The order management module will support:
|
The order management module will support:
|
||||||
|
|
||||||
- Listing orders with filters (payment status, order type, date range, order number)
|
- Listing orders with filters (payment status, order type, date range, order number)
|
||||||
- Viewing order details including buyer information, order items (packages), and payment details
|
- Viewing order details including buyer information, order items (packages), and payment details
|
||||||
- Creating orders for single card or device purchases with package selection
|
- Creating orders for single card or device purchases with package selection
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
实现完整的套餐管理系统,包括4个核心模块。该系统需要支持多级代理商体系的套餐分配和定价管理。
|
实现完整的套餐管理系统,包括4个核心模块。该系统需要支持多级代理商体系的套餐分配和定价管理。
|
||||||
|
|
||||||
**背景**:
|
**背景**:
|
||||||
|
|
||||||
- 项目已有类型定义(src/types/api/package.ts),但使用不同的字段命名和枚举值
|
- 项目已有类型定义(src/types/api/package.ts),但使用不同的字段命名和枚举值
|
||||||
- 后端 API 已实现,使用下划线命名(如 `series_name`)
|
- 后端 API 已实现,使用下划线命名(如 `series_name`)
|
||||||
- 前端项目统一使用 CommonStatus 枚举(0:禁用, 1:启用)
|
- 前端项目统一使用 CommonStatus 枚举(0:禁用, 1:启用)
|
||||||
- 参考实现:`/system/role` 页面使用了组件化架构
|
- 参考实现:`/system/role` 页面使用了组件化架构
|
||||||
|
|
||||||
**约束**:
|
**约束**:
|
||||||
|
|
||||||
- 必须保留现有类型定义文件,不能破坏现有代码
|
- 必须保留现有类型定义文件,不能破坏现有代码
|
||||||
- 需要兼容后端 API 的字段命名规范
|
- 需要兼容后端 API 的字段命名规范
|
||||||
- 需要适配项目的状态枚举规范
|
- 需要适配项目的状态枚举规范
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
## Goals / Non-Goals
|
## Goals / Non-Goals
|
||||||
|
|
||||||
### Goals
|
### Goals
|
||||||
|
|
||||||
1. 实现4个核心模块的完整 CRUD 功能
|
1. 实现4个核心模块的完整 CRUD 功能
|
||||||
2. 建立统一的 API 服务层,封装后端接口
|
2. 建立统一的 API 服务层,封装后端接口
|
||||||
3. 实现组件化的页面结构,参考 `/system/role`
|
3. 实现组件化的页面结构,参考 `/system/role`
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
5. 确保数据隔离和权限控制
|
5. 确保数据隔离和权限控制
|
||||||
|
|
||||||
### Non-Goals
|
### Non-Goals
|
||||||
|
|
||||||
1. 不重构现有的 package.ts 类型定义
|
1. 不重构现有的 package.ts 类型定义
|
||||||
2. 不实现套餐的实时统计和报表功能(后续迭代)
|
2. 不实现套餐的实时统计和报表功能(后续迭代)
|
||||||
3. 不实现套餐批量导入功能(后续迭代)
|
3. 不实现套餐批量导入功能(后续迭代)
|
||||||
@@ -37,16 +41,19 @@
|
|||||||
**问题**:后端使用下划线命名(snake_case),前端类型通常使用驼峰命名(camelCase)。
|
**问题**:后端使用下划线命名(snake_case),前端类型通常使用驼峰命名(camelCase)。
|
||||||
|
|
||||||
**决策**:
|
**决策**:
|
||||||
|
|
||||||
- API 请求/响应保持下划线命名,与后端保持一致
|
- API 请求/响应保持下划线命名,与后端保持一致
|
||||||
- 创建新的类型文件 `packageManagement.ts`,使用下划线命名
|
- 创建新的类型文件 `packageManagement.ts`,使用下划线命名
|
||||||
- 在表单提交和响应处理时不做转换,直接使用下划线字段
|
- 在表单提交和响应处理时不做转换,直接使用下划线字段
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 减少转换层的复杂性和错误风险
|
- 减少转换层的复杂性和错误风险
|
||||||
- 与后端 API 文档保持一致,便于对照
|
- 与后端 API 文档保持一致,便于对照
|
||||||
- TypeScript 支持下划线字段名,不影响类型安全
|
- TypeScript 支持下划线字段名,不影响类型安全
|
||||||
|
|
||||||
**示例**:
|
**示例**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export interface PackageSeriesResponse {
|
export interface PackageSeriesResponse {
|
||||||
id: number
|
id: number
|
||||||
@@ -63,11 +70,13 @@ export interface PackageSeriesResponse {
|
|||||||
**问题**:文档中状态是 `1:启用, 2:禁用`,但项目 CommonStatus 是 `0:禁用, 1:启用`。
|
**问题**:文档中状态是 `1:启用, 2:禁用`,但项目 CommonStatus 是 `0:禁用, 1:启用`。
|
||||||
|
|
||||||
**决策**:
|
**决策**:
|
||||||
|
|
||||||
- **在常量配置中定义套餐专用的状态枚举**
|
- **在常量配置中定义套餐专用的状态枚举**
|
||||||
- **前端页面使用项目统一的 CommonStatus(0/1)**
|
- **前端页面使用项目统一的 CommonStatus(0/1)**
|
||||||
- **在 API 服务层进行状态值映射转换**
|
- **在 API 服务层进行状态值映射转换**
|
||||||
|
|
||||||
**映射规则**:
|
**映射规则**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 前端 -> 后端
|
// 前端 -> 后端
|
||||||
CommonStatus.ENABLED (1) -> API Status (1)
|
CommonStatus.ENABLED (1) -> API Status (1)
|
||||||
@@ -79,6 +88,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 保持前端 UI 的一致性
|
- 保持前端 UI 的一致性
|
||||||
- 避免混淆项目开发者
|
- 避免混淆项目开发者
|
||||||
- 集中在 API 服务层处理差异
|
- 集中在 API 服务层处理差异
|
||||||
@@ -88,18 +98,21 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**问题**:是创建单个 package.ts 服务,还是拆分为多个服务文件?
|
**问题**:是创建单个 package.ts 服务,还是拆分为多个服务文件?
|
||||||
|
|
||||||
**决策**:拆分为4个独立的服务文件:
|
**决策**:拆分为4个独立的服务文件:
|
||||||
|
|
||||||
1. `packageSeries.ts` - 套餐系列管理
|
1. `packageSeries.ts` - 套餐系列管理
|
||||||
2. `package.ts` - 套餐管理
|
2. `package.ts` - 套餐管理
|
||||||
3. `myPackage.ts` - 代理可售套餐
|
3. `myPackage.ts` - 代理可售套餐
|
||||||
4. `shopPackageAllocation.ts` - 单套餐分配
|
4. `shopPackageAllocation.ts` - 单套餐分配
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 每个模块功能独立,职责清晰
|
- 每个模块功能独立,职责清晰
|
||||||
- 便于维护和扩展
|
- 便于维护和扩展
|
||||||
- 符合单一职责原则
|
- 符合单一职责原则
|
||||||
- 便于团队协作(不同开发者负责不同模块)
|
- 便于团队协作(不同开发者负责不同模块)
|
||||||
|
|
||||||
**替代方案**:
|
**替代方案**:
|
||||||
|
|
||||||
- 单个 package.ts 文件 - **拒绝**,文件过大,难以维护
|
- 单个 package.ts 文件 - **拒绝**,文件过大,难以维护
|
||||||
|
|
||||||
### Decision 4: 定价规则实现
|
### Decision 4: 定价规则实现
|
||||||
@@ -107,11 +120,13 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**问题**:代理商的套餐成本价有两种计算方式:系列加价和单套餐覆盖。
|
**问题**:代理商的套餐成本价有两种计算方式:系列加价和单套餐覆盖。
|
||||||
|
|
||||||
**决策**:
|
**决策**:
|
||||||
|
|
||||||
- **后端负责成本价计算**,前端只展示结果
|
- **后端负责成本价计算**,前端只展示结果
|
||||||
- 前端接收 `price_source` 字段,标识价格来源
|
- 前端接收 `price_source` 字段,标识价格来源
|
||||||
- 单套餐分配创建时,保存 `calculated_cost_price`(系列规则计算的价格)供参考
|
- 单套餐分配创建时,保存 `calculated_cost_price`(系列规则计算的价格)供参考
|
||||||
|
|
||||||
**数据流**:
|
**数据流**:
|
||||||
|
|
||||||
```
|
```
|
||||||
1. 系列分配:pricing_mode + pricing_value -> 后端计算 -> cost_price
|
1. 系列分配:pricing_mode + pricing_value -> 后端计算 -> cost_price
|
||||||
2. 单套餐分配:直接设置 cost_price(覆盖系列规则)
|
2. 单套餐分配:直接设置 cost_price(覆盖系列规则)
|
||||||
@@ -119,6 +134,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 计算逻辑复杂,集中在后端便于维护
|
- 计算逻辑复杂,集中在后端便于维护
|
||||||
- 前端只负责展示,降低复杂度
|
- 前端只负责展示,降低复杂度
|
||||||
- 保留 calculated_cost_price 便于调试和审计
|
- 保留 calculated_cost_price 便于调试和审计
|
||||||
@@ -128,16 +144,19 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**问题**:客户端验证 vs 服务端验证。
|
**问题**:客户端验证 vs 服务端验证。
|
||||||
|
|
||||||
**决策**:**双重验证**
|
**决策**:**双重验证**
|
||||||
|
|
||||||
- 客户端:使用 Element Plus 的 FormRules 进行基础验证
|
- 客户端:使用 Element Plus 的 FormRules 进行基础验证
|
||||||
- 服务端:后端 API 进行完整验证并返回详细错误
|
- 服务端:后端 API 进行完整验证并返回详细错误
|
||||||
|
|
||||||
**客户端验证规则**:
|
**客户端验证规则**:
|
||||||
|
|
||||||
- 必填字段检查
|
- 必填字段检查
|
||||||
- 长度限制(如系列名称 1-255 字符)
|
- 长度限制(如系列名称 1-255 字符)
|
||||||
- 数值范围(如套餐时长 1-120 月)
|
- 数值范围(如套餐时长 1-120 月)
|
||||||
- 格式验证(如价格必须为正整数)
|
- 格式验证(如价格必须为正整数)
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 客户端验证提升用户体验,即时反馈
|
- 客户端验证提升用户体验,即时反馈
|
||||||
- 服务端验证保证数据安全性和完整性
|
- 服务端验证保证数据安全性和完整性
|
||||||
- 符合 Web 应用最佳实践
|
- 符合 Web 应用最佳实践
|
||||||
@@ -147,20 +166,26 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**问题**:页面结构如何组织?
|
**问题**:页面结构如何组织?
|
||||||
|
|
||||||
**决策**:参考 `/system/role` 页面,使用组件化结构:
|
**决策**:参考 `/system/role` 页面,使用组件化结构:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<ArtTableFullScreen>
|
<ArtTableFullScreen>
|
||||||
<ArtSearchBar /> <!-- 搜索栏 -->
|
<ArtSearchBar />
|
||||||
|
<!-- 搜索栏 -->
|
||||||
<ElCard>
|
<ElCard>
|
||||||
<ArtTableHeader /> <!-- 表格头部:刷新、列设置、操作按钮 -->
|
<ArtTableHeader />
|
||||||
<ArtTable /> <!-- 数据表格 -->
|
<!-- 表格头部:刷新、列设置、操作按钮 -->
|
||||||
<ElDialog /> <!-- 新增/编辑对话框 -->
|
<ArtTable />
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<ElDialog />
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 与项目现有页面风格一致
|
- 与项目现有页面风格一致
|
||||||
- 复用成熟的组件,减少开发工作量
|
- 复用成熟的组件,减少开发工作量
|
||||||
- 便于维护和扩展
|
- 便于维护和扩展
|
||||||
@@ -172,6 +197,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**风险**:后端接口可能尚未实现或与文档不一致。
|
**风险**:后端接口可能尚未实现或与文档不一致。
|
||||||
|
|
||||||
**缓解措施**:
|
**缓解措施**:
|
||||||
|
|
||||||
1. 先实现 API 服务层,使用 TypeScript 类型约束
|
1. 先实现 API 服务层,使用 TypeScript 类型约束
|
||||||
2. 使用 Mock 数据进行前端开发(已有示例)
|
2. 使用 Mock 数据进行前端开发(已有示例)
|
||||||
3. 与后端团队确认 API 规范和联调时间
|
3. 与后端团队确认 API 规范和联调时间
|
||||||
@@ -182,6 +208,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**风险**:在某些地方忘记转换状态值,导致显示错误。
|
**风险**:在某些地方忘记转换状态值,导致显示错误。
|
||||||
|
|
||||||
**缓解措施**:
|
**缓解措施**:
|
||||||
|
|
||||||
1. 在 API 服务层统一处理转换
|
1. 在 API 服务层统一处理转换
|
||||||
2. 创建工具函数封装映射逻辑
|
2. 创建工具函数封装映射逻辑
|
||||||
3. 编写单元测试覆盖映射函数
|
3. 编写单元测试覆盖映射函数
|
||||||
@@ -192,6 +219,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**风险**:对定价规则的理解与实际业务需求有偏差。
|
**风险**:对定价规则的理解与实际业务需求有偏差。
|
||||||
|
|
||||||
**缓解措施**:
|
**缓解措施**:
|
||||||
|
|
||||||
1. 在实现前与产品确认定价规则
|
1. 在实现前与产品确认定价规则
|
||||||
2. 编写测试用例覆盖各种定价场景
|
2. 编写测试用例覆盖各种定价场景
|
||||||
3. 在 UI 上清晰展示价格来源和计算方式
|
3. 在 UI 上清晰展示价格来源和计算方式
|
||||||
@@ -202,10 +230,12 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**取舍**:保留旧的 package.ts 类型定义,新增 packageManagement.ts。
|
**取舍**:保留旧的 package.ts 类型定义,新增 packageManagement.ts。
|
||||||
|
|
||||||
**代价**:
|
**代价**:
|
||||||
|
|
||||||
- 存在两套类型定义,可能造成混淆
|
- 存在两套类型定义,可能造成混淆
|
||||||
- 占用额外的代码空间
|
- 占用额外的代码空间
|
||||||
|
|
||||||
**收益**:
|
**收益**:
|
||||||
|
|
||||||
- 不影响现有代码,向后兼容
|
- 不影响现有代码,向后兼容
|
||||||
- 新旧系统可以并存,降低迁移风险
|
- 新旧系统可以并存,降低迁移风险
|
||||||
- 未来可以逐步迁移到新类型
|
- 未来可以逐步迁移到新类型
|
||||||
@@ -215,10 +245,12 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**取舍**:在 API 服务层进行状态值转换。
|
**取舍**:在 API 服务层进行状态值转换。
|
||||||
|
|
||||||
**代价**:
|
**代价**:
|
||||||
|
|
||||||
- 增加一层转换逻辑
|
- 增加一层转换逻辑
|
||||||
- 可能影响性能(微小)
|
- 可能影响性能(微小)
|
||||||
|
|
||||||
**收益**:
|
**收益**:
|
||||||
|
|
||||||
- 前端 UI 保持一致性
|
- 前端 UI 保持一致性
|
||||||
- 业务逻辑更清晰
|
- 业务逻辑更清晰
|
||||||
- 便于后续维护
|
- 便于后续维护
|
||||||
@@ -226,27 +258,32 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
### Phase 1: 基础设施(1-2天)
|
### Phase 1: 基础设施(1-2天)
|
||||||
|
|
||||||
1. 创建类型定义文件
|
1. 创建类型定义文件
|
||||||
2. 创建常量配置文件
|
2. 创建常量配置文件
|
||||||
3. 设置状态映射工具函数
|
3. 设置状态映射工具函数
|
||||||
|
|
||||||
### Phase 2: API 服务层(2-3天)
|
### Phase 2: API 服务层(2-3天)
|
||||||
|
|
||||||
1. 实现4个 API 服务模块
|
1. 实现4个 API 服务模块
|
||||||
2. 编写单元测试(可选)
|
2. 编写单元测试(可选)
|
||||||
3. 使用 Mock 数据测试
|
3. 使用 Mock 数据测试
|
||||||
|
|
||||||
### Phase 3: 页面实现(4-5天)
|
### Phase 3: 页面实现(4-5天)
|
||||||
|
|
||||||
1. 套餐系列管理页面(1天)
|
1. 套餐系列管理页面(1天)
|
||||||
2. 套餐管理页面(1.5天)
|
2. 套餐管理页面(1.5天)
|
||||||
3. 代理可售套餐页面(1天)
|
3. 代理可售套餐页面(1天)
|
||||||
4. 单套餐分配页面(1.5天)
|
4. 单套餐分配页面(1.5天)
|
||||||
|
|
||||||
### Phase 4: 集成测试(1-2天)
|
### Phase 4: 集成测试(1-2天)
|
||||||
|
|
||||||
1. 与后端 API 联调
|
1. 与后端 API 联调
|
||||||
2. 端到端功能测试
|
2. 端到端功能测试
|
||||||
3. 修复 Bug 和优化
|
3. 修复 Bug 和优化
|
||||||
|
|
||||||
### Phase 5: 上线(1天)
|
### Phase 5: 上线(1天)
|
||||||
|
|
||||||
1. Code Review
|
1. Code Review
|
||||||
2. 合并代码
|
2. 合并代码
|
||||||
3. 部署到测试环境
|
3. 部署到测试环境
|
||||||
@@ -257,6 +294,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
### Rollback Plan
|
### Rollback Plan
|
||||||
|
|
||||||
如果出现严重问题,回滚步骤:
|
如果出现严重问题,回滚步骤:
|
||||||
|
|
||||||
1. 从 Git 回滚到上一个稳定版本
|
1. 从 Git 回滚到上一个稳定版本
|
||||||
2. 移除新增的路由配置
|
2. 移除新增的路由配置
|
||||||
3. 移除新增的 API 服务导出
|
3. 移除新增的 API 服务导出
|
||||||
@@ -267,6 +305,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
**问题**:如何统一处理各类错误和异常?
|
**问题**:如何统一处理各类错误和异常?
|
||||||
|
|
||||||
**决策**:分层错误处理机制
|
**决策**:分层错误处理机制
|
||||||
|
|
||||||
- **网络错误**:axios 拦截器统一捕获,显示通用错误提示
|
- **网络错误**:axios 拦截器统一捕获,显示通用错误提示
|
||||||
- **401 未认证**:自动跳转到登录页面
|
- **401 未认证**:自动跳转到登录页面
|
||||||
- **403 无权限**:显示权限不足提示,不跳转
|
- **403 无权限**:显示权限不足提示,不跳转
|
||||||
@@ -274,6 +313,7 @@ API Status (2) -> CommonStatus.DISABLED (0)
|
|||||||
- **表单验证错误**:在表单字段下显示错误提示
|
- **表单验证错误**:在表单字段下显示错误提示
|
||||||
|
|
||||||
**错误提示方式**:
|
**错误提示方式**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 网络错误或服务器错误
|
// 网络错误或服务器错误
|
||||||
ElMessage.error('网络错误,请稍后重试')
|
ElMessage.error('网络错误,请稍后重试')
|
||||||
@@ -286,6 +326,7 @@ ElMessage.success('操作成功')
|
|||||||
```
|
```
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 统一的错误处理提升用户体验
|
- 统一的错误处理提升用户体验
|
||||||
- 分层处理避免重复代码
|
- 分层处理避免重复代码
|
||||||
- 清晰的错误提示帮助用户理解问题
|
- 清晰的错误提示帮助用户理解问题
|
||||||
@@ -297,6 +338,7 @@ ElMessage.success('操作成功')
|
|||||||
**决策**:细粒度的 loading 状态管理
|
**决策**:细粒度的 loading 状态管理
|
||||||
|
|
||||||
**Loading 状态分类**:
|
**Loading 状态分类**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const loading = ref(false) // 表格数据加载
|
const loading = ref(false) // 表格数据加载
|
||||||
const submitLoading = ref(false) // 表单提交
|
const submitLoading = ref(false) // 表单提交
|
||||||
@@ -304,29 +346,26 @@ const deleteLoading = ref<Record<number, boolean>>({}) // 删除操作(可选
|
|||||||
```
|
```
|
||||||
|
|
||||||
**状态管理规则**:
|
**状态管理规则**:
|
||||||
|
|
||||||
- **列表查询**:表格显示 loading 遮罩
|
- **列表查询**:表格显示 loading 遮罩
|
||||||
- **新增/编辑提交**:提交按钮显示 loading,禁用表单
|
- **新增/编辑提交**:提交按钮显示 loading,禁用表单
|
||||||
- **删除操作**:可选择在按钮上显示 loading 或全局 loading
|
- **删除操作**:可选择在按钮上显示 loading 或全局 loading
|
||||||
- **状态切换**:ElSwitch 自带 loading 效果,先更新 UI 再调用 API
|
- **状态切换**:ElSwitch 自带 loading 效果,先更新 UI 再调用 API
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 细粒度控制提供更好的交互反馈
|
- 细粒度控制提供更好的交互反馈
|
||||||
- 防止重复提交
|
- 防止重复提交
|
||||||
- 清晰标识正在进行的操作
|
- 清晰标识正在进行的操作
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
1. **Q**: 套餐被删除后,历史订单如何处理?
|
1. **Q**: 套餐被删除后,历史订单如何处理? **A**: 待产品确认,可能需要软删除机制
|
||||||
**A**: 待产品确认,可能需要软删除机制
|
|
||||||
|
|
||||||
2. **Q**: 代理商可以自行调整套餐售价吗?
|
2. **Q**: 代理商可以自行调整套餐售价吗? **A**: 待产品确认,当前设计只展示建议售价
|
||||||
**A**: 待产品确认,当前设计只展示建议售价
|
|
||||||
|
|
||||||
3. **Q**: 套餐系列和套餐是否支持批量操作(批量启用/禁用)?
|
3. **Q**: 套餐系列和套餐是否支持批量操作(批量启用/禁用)? **A**: 当前不支持,后续迭代考虑
|
||||||
**A**: 当前不支持,后续迭代考虑
|
|
||||||
|
|
||||||
4. **Q**: 是否需要套餐变更历史记录?
|
4. **Q**: 是否需要套餐变更历史记录? **A**: 后端可能有审计日志,前端暂不展示
|
||||||
**A**: 后端可能有审计日志,前端暂不展示
|
|
||||||
|
|
||||||
5. **Q**: 单套餐分配的"原计算成本价"是否需要实时更新?
|
5. **Q**: 单套餐分配的"原计算成本价"是否需要实时更新? **A**: 待确认,当前设计是创建时计算一次,不自动更新
|
||||||
**A**: 待确认,当前设计是创建时计算一次,不自动更新
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
采用模块化设计,拆分为 4 个独立的 API 服务文件:
|
采用模块化设计,拆分为 4 个独立的 API 服务文件:
|
||||||
|
|
||||||
- **新增**: `src/api/modules/packageSeries.ts` - 套餐系列 API 服务
|
- **新增**: `src/api/modules/packageSeries.ts` - 套餐系列 API 服务
|
||||||
|
|
||||||
- 套餐系列列表查询(分页、筛选)
|
- 套餐系列列表查询(分页、筛选)
|
||||||
- 创建套餐系列
|
- 创建套餐系列
|
||||||
- 获取套餐系列详情
|
- 获取套餐系列详情
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
- 更新套餐系列状态
|
- 更新套餐系列状态
|
||||||
|
|
||||||
- **新增**: `src/api/modules/package.ts` - 套餐管理 API 服务
|
- **新增**: `src/api/modules/package.ts` - 套餐管理 API 服务
|
||||||
|
|
||||||
- 套餐列表查询(分页、多条件筛选)
|
- 套餐列表查询(分页、多条件筛选)
|
||||||
- 创建套餐
|
- 创建套餐
|
||||||
- 获取套餐详情
|
- 获取套餐详情
|
||||||
@@ -36,11 +38,13 @@
|
|||||||
- 获取系列下拉选项(用于表单选择)
|
- 获取系列下拉选项(用于表单选择)
|
||||||
|
|
||||||
- **新增**: `src/api/modules/myPackage.ts` - 代理可售套餐 API 服务
|
- **新增**: `src/api/modules/myPackage.ts` - 代理可售套餐 API 服务
|
||||||
|
|
||||||
- 我的可售套餐列表查询
|
- 我的可售套餐列表查询
|
||||||
- 获取可售套餐详情
|
- 获取可售套餐详情
|
||||||
- 我的被分配系列列表
|
- 我的被分配系列列表
|
||||||
|
|
||||||
- **新增**: `src/api/modules/shopPackageAllocation.ts` - 单套餐分配 API 服务
|
- **新增**: `src/api/modules/shopPackageAllocation.ts` - 单套餐分配 API 服务
|
||||||
|
|
||||||
- 单套餐分配列表查询
|
- 单套餐分配列表查询
|
||||||
- 创建单套餐分配
|
- 创建单套餐分配
|
||||||
- 获取单套餐分配详情
|
- 获取单套餐分配详情
|
||||||
@@ -64,12 +68,14 @@
|
|||||||
### 3. 页面实现
|
### 3. 页面实现
|
||||||
|
|
||||||
**套餐系列管理** (`src/views/package-management/package-series/index.vue`)
|
**套餐系列管理** (`src/views/package-management/package-series/index.vue`)
|
||||||
|
|
||||||
- 列表展示(支持名称搜索、状态筛选)
|
- 列表展示(支持名称搜索、状态筛选)
|
||||||
- 新增/编辑套餐系列
|
- 新增/编辑套餐系列
|
||||||
- 删除套餐系列
|
- 删除套餐系列
|
||||||
- 状态开关(启用/禁用)
|
- 状态开关(启用/禁用)
|
||||||
|
|
||||||
**套餐管理** (`src/views/package-management/package-list/index.vue`)
|
**套餐管理** (`src/views/package-management/package-list/index.vue`)
|
||||||
|
|
||||||
- 列表展示(支持多条件筛选:名称、系列、状态、上架状态、套餐类型)
|
- 列表展示(支持多条件筛选:名称、系列、状态、上架状态、套餐类型)
|
||||||
- 新增/编辑套餐
|
- 新增/编辑套餐
|
||||||
- 删除套餐
|
- 删除套餐
|
||||||
@@ -77,11 +83,13 @@
|
|||||||
- 上架状态开关(上架/下架)
|
- 上架状态开关(上架/下架)
|
||||||
|
|
||||||
**代理可售套餐** (`src/views/package-management/my-packages/index.vue`)
|
**代理可售套餐** (`src/views/package-management/my-packages/index.vue`)
|
||||||
|
|
||||||
- 查看被分配的套餐列表(支持系列、类型筛选)
|
- 查看被分配的套餐列表(支持系列、类型筛选)
|
||||||
- 查看套餐详情(成本价、建议售价、利润空间等)
|
- 查看套餐详情(成本价、建议售价、利润空间等)
|
||||||
- 查看被分配系列列表
|
- 查看被分配系列列表
|
||||||
|
|
||||||
**单套餐分配** (`src/views/package-management/package-assign/index.vue`)
|
**单套餐分配** (`src/views/package-management/package-assign/index.vue`)
|
||||||
|
|
||||||
- 分配列表(支持店铺、套餐、状态筛选)
|
- 分配列表(支持店铺、套餐、状态筛选)
|
||||||
- 创建分配(选择套餐、店铺、设置成本价)
|
- 创建分配(选择套餐、店铺、设置成本价)
|
||||||
- 编辑分配(修改成本价)
|
- 编辑分配(修改成本价)
|
||||||
@@ -100,12 +108,15 @@
|
|||||||
### 5. 路由配置
|
### 5. 路由配置
|
||||||
|
|
||||||
已存在的路由(无需修改):
|
已存在的路由(无需修改):
|
||||||
|
|
||||||
- `/package-management/package-series` - 套餐系列管理
|
- `/package-management/package-series` - 套餐系列管理
|
||||||
- `/package-management/package-list` - 套餐管理
|
- `/package-management/package-list` - 套餐管理
|
||||||
- `/package-management/package-assign` - 单套餐分配
|
- `/package-management/package-assign` - 单套餐分配
|
||||||
|
|
||||||
需要新增的路由:
|
需要新增的路由:
|
||||||
|
|
||||||
- **新增**: `src/router/routesAlias.ts` - 添加路由别名
|
- **新增**: `src/router/routesAlias.ts` - 添加路由别名
|
||||||
|
|
||||||
- `MyPackages = '/package-management/my-packages'` - 代理可售套餐
|
- `MyPackages = '/package-management/my-packages'` - 代理可售套餐
|
||||||
|
|
||||||
- **新增**: `src/router/routes/asyncRoutes.ts` - 添加路由配置
|
- **新增**: `src/router/routes/asyncRoutes.ts` - 添加路由配置
|
||||||
@@ -114,12 +125,14 @@
|
|||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
### 受影响的规范
|
### 受影响的规范
|
||||||
|
|
||||||
- `package-series-management` - 新增能力
|
- `package-series-management` - 新增能力
|
||||||
- `package-management` - 新增能力
|
- `package-management` - 新增能力
|
||||||
- `my-packages` - 新增能力
|
- `my-packages` - 新增能力
|
||||||
- `shop-package-allocation` - 新增能力
|
- `shop-package-allocation` - 新增能力
|
||||||
|
|
||||||
### 受影响的代码
|
### 受影响的代码
|
||||||
|
|
||||||
- `src/api/modules/*` - 新增 4 个 API 服务模块
|
- `src/api/modules/*` - 新增 4 个 API 服务模块
|
||||||
- `src/types/api/*` - 新增类型定义文件
|
- `src/types/api/*` - 新增类型定义文件
|
||||||
- `src/views/package-management/*` - 4 个页面完整实现
|
- `src/views/package-management/*` - 4 个页面完整实现
|
||||||
@@ -127,6 +140,7 @@
|
|||||||
- `src/router/routes/asyncRoutes.ts` - 路由配置
|
- `src/router/routes/asyncRoutes.ts` - 路由配置
|
||||||
|
|
||||||
### 依赖关系
|
### 依赖关系
|
||||||
|
|
||||||
- 依赖现有的组件库(ArtTable、ArtSearchBar、ArtTableHeader 等)
|
- 依赖现有的组件库(ArtTable、ArtSearchBar、ArtTableHeader 等)
|
||||||
- 依赖现有的 HTTP 请求工具(request.ts)
|
- 依赖现有的 HTTP 请求工具(request.ts)
|
||||||
- 依赖现有的权限控制和路由守卫
|
- 依赖现有的权限控制和路由守卫
|
||||||
@@ -134,10 +148,12 @@
|
|||||||
- 后端 API 需已实现(docs/套餐.md 中定义的接口)
|
- 后端 API 需已实现(docs/套餐.md 中定义的接口)
|
||||||
|
|
||||||
**注意事项**:
|
**注意事项**:
|
||||||
|
|
||||||
- ShopService 应该已经存在于 src/api/modules/shop.ts
|
- ShopService 应该已经存在于 src/api/modules/shop.ts
|
||||||
- 如果不存在,需要先实现或使用 Mock 数据
|
- 如果不存在,需要先实现或使用 Mock 数据
|
||||||
|
|
||||||
### 风险评估
|
### 风险评估
|
||||||
|
|
||||||
- **低风险**: 独立模块,不影响现有功能
|
- **低风险**: 独立模块,不影响现有功能
|
||||||
- **API 依赖**: 需确保后端接口已实现并联调
|
- **API 依赖**: 需确保后端接口已实现并联调
|
||||||
- **权限控制**: 需配置对应的菜单和按钮权限
|
- **权限控制**: 需配置对应的菜单和按钮权限
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
## 2. API 服务层实现
|
## 2. API 服务层实现
|
||||||
|
|
||||||
### 2.1 套餐系列 API(packageSeries.ts)
|
### 2.1 套餐系列 API(packageSeries.ts)
|
||||||
|
|
||||||
- [ ] 2.1.1 实现 getPackageSeries(套餐系列列表)
|
- [ ] 2.1.1 实现 getPackageSeries(套餐系列列表)
|
||||||
- [ ] 2.1.2 实现 createPackageSeries(创建套餐系列)
|
- [ ] 2.1.2 实现 createPackageSeries(创建套餐系列)
|
||||||
- [ ] 2.1.3 实现 getPackageSeriesDetail(获取套餐系列详情)
|
- [ ] 2.1.3 实现 getPackageSeriesDetail(获取套餐系列详情)
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
- [ ] 2.1.6 实现 updatePackageSeriesStatus(更新套餐系列状态)
|
- [ ] 2.1.6 实现 updatePackageSeriesStatus(更新套餐系列状态)
|
||||||
|
|
||||||
### 2.2 套餐管理 API(package.ts)
|
### 2.2 套餐管理 API(package.ts)
|
||||||
|
|
||||||
- [ ] 2.2.1 实现 getPackages(套餐列表)
|
- [ ] 2.2.1 实现 getPackages(套餐列表)
|
||||||
- [ ] 2.2.2 实现 createPackage(创建套餐)
|
- [ ] 2.2.2 实现 createPackage(创建套餐)
|
||||||
- [ ] 2.2.3 实现 getPackageDetail(获取套餐详情)
|
- [ ] 2.2.3 实现 getPackageDetail(获取套餐详情)
|
||||||
@@ -26,11 +28,13 @@
|
|||||||
- [ ] 2.2.7 实现 updatePackageShelfStatus(更新套餐上架状态)
|
- [ ] 2.2.7 实现 updatePackageShelfStatus(更新套餐上架状态)
|
||||||
|
|
||||||
### 2.3 代理可售套餐 API(myPackage.ts)
|
### 2.3 代理可售套餐 API(myPackage.ts)
|
||||||
|
|
||||||
- [ ] 2.3.1 实现 getMyPackages(我的可售套餐列表)
|
- [ ] 2.3.1 实现 getMyPackages(我的可售套餐列表)
|
||||||
- [ ] 2.3.2 实现 getMyPackageDetail(获取可售套餐详情)
|
- [ ] 2.3.2 实现 getMyPackageDetail(获取可售套餐详情)
|
||||||
- [ ] 2.3.3 实现 getMySeriesAllocations(我的被分配系列列表)
|
- [ ] 2.3.3 实现 getMySeriesAllocations(我的被分配系列列表)
|
||||||
|
|
||||||
### 2.4 单套餐分配 API(shopPackageAllocation.ts)
|
### 2.4 单套餐分配 API(shopPackageAllocation.ts)
|
||||||
|
|
||||||
- [ ] 2.4.1 实现 getShopPackageAllocations(单套餐分配列表)
|
- [ ] 2.4.1 实现 getShopPackageAllocations(单套餐分配列表)
|
||||||
- [ ] 2.4.2 实现 createShopPackageAllocation(创建单套餐分配)
|
- [ ] 2.4.2 实现 createShopPackageAllocation(创建单套餐分配)
|
||||||
- [ ] 2.4.3 实现 getShopPackageAllocationDetail(获取单套餐分配详情)
|
- [ ] 2.4.3 实现 getShopPackageAllocationDetail(获取单套餐分配详情)
|
||||||
@@ -43,6 +47,7 @@
|
|||||||
## 3. 页面实现
|
## 3. 页面实现
|
||||||
|
|
||||||
### 3.1 套餐系列管理页面(package-series/index.vue)
|
### 3.1 套餐系列管理页面(package-series/index.vue)
|
||||||
|
|
||||||
- [ ] 3.1.1 实现列表展示(表格、分页)
|
- [ ] 3.1.1 实现列表展示(表格、分页)
|
||||||
- [ ] 3.1.2 实现搜索栏(系列名称、状态筛选)
|
- [ ] 3.1.2 实现搜索栏(系列名称、状态筛选)
|
||||||
- [ ] 3.1.3 实现新增对话框(表单验证)
|
- [ ] 3.1.3 实现新增对话框(表单验证)
|
||||||
@@ -52,6 +57,7 @@
|
|||||||
- [ ] 3.1.7 集成 API 服务并处理加载状态
|
- [ ] 3.1.7 集成 API 服务并处理加载状态
|
||||||
|
|
||||||
### 3.2 套餐管理页面(package-list/index.vue)
|
### 3.2 套餐管理页面(package-list/index.vue)
|
||||||
|
|
||||||
- [ ] 3.2.1 实现列表展示(表格、分页)
|
- [ ] 3.2.1 实现列表展示(表格、分页)
|
||||||
- [ ] 3.2.2 实现搜索栏(名称、系列、状态、上架状态、类型筛选)
|
- [ ] 3.2.2 实现搜索栏(名称、系列、状态、上架状态、类型筛选)
|
||||||
- [ ] 3.2.3 实现系列下拉选择器(加载套餐系列列表,只显示启用状态)
|
- [ ] 3.2.3 实现系列下拉选择器(加载套餐系列列表,只显示启用状态)
|
||||||
@@ -63,6 +69,7 @@
|
|||||||
- [ ] 3.2.9 集成 API 服务并处理加载状态
|
- [ ] 3.2.9 集成 API 服务并处理加载状态
|
||||||
|
|
||||||
### 3.3 代理可售套餐页面(my-packages/index.vue)
|
### 3.3 代理可售套餐页面(my-packages/index.vue)
|
||||||
|
|
||||||
- [ ] 3.3.1 创建页面文件和基本结构
|
- [ ] 3.3.1 创建页面文件和基本结构
|
||||||
- [ ] 3.3.2 实现列表展示(表格、分页)
|
- [ ] 3.3.2 实现列表展示(表格、分页)
|
||||||
- [ ] 3.3.3 实现搜索栏(系列、类型筛选)
|
- [ ] 3.3.3 实现搜索栏(系列、类型筛选)
|
||||||
@@ -71,6 +78,7 @@
|
|||||||
- [ ] 3.3.6 集成 API 服务并处理加载状态
|
- [ ] 3.3.6 集成 API 服务并处理加载状态
|
||||||
|
|
||||||
### 3.4 单套餐分配页面(package-assign/index.vue)
|
### 3.4 单套餐分配页面(package-assign/index.vue)
|
||||||
|
|
||||||
- [ ] 3.4.1 创建页面文件和基本结构
|
- [ ] 3.4.1 创建页面文件和基本结构
|
||||||
- [ ] 3.4.2 实现列表展示(表格、分页)
|
- [ ] 3.4.2 实现列表展示(表格、分页)
|
||||||
- [ ] 3.4.3 实现搜索栏(店铺、套餐、状态筛选)
|
- [ ] 3.4.3 实现搜索栏(店铺、套餐、状态筛选)
|
||||||
@@ -90,6 +98,7 @@
|
|||||||
## 5. 集成测试
|
## 5. 集成测试
|
||||||
|
|
||||||
### 5.1 套餐系列管理测试
|
### 5.1 套餐系列管理测试
|
||||||
|
|
||||||
- [ ] 5.1.1 测试列表查询(空列表、有数据、分页)
|
- [ ] 5.1.1 测试列表查询(空列表、有数据、分页)
|
||||||
- [ ] 5.1.2 测试搜索功能(名称模糊搜索、状态筛选)
|
- [ ] 5.1.2 测试搜索功能(名称模糊搜索、状态筛选)
|
||||||
- [ ] 5.1.3 测试新增功能(成功、编码重复、字段验证)
|
- [ ] 5.1.3 测试新增功能(成功、编码重复、字段验证)
|
||||||
@@ -99,6 +108,7 @@
|
|||||||
- [ ] 5.1.7 测试权限控制(未登录、无权限)
|
- [ ] 5.1.7 测试权限控制(未登录、无权限)
|
||||||
|
|
||||||
### 5.2 套餐管理测试
|
### 5.2 套餐管理测试
|
||||||
|
|
||||||
- [ ] 5.2.1 测试列表查询(空列表、有数据、分页)
|
- [ ] 5.2.1 测试列表查询(空列表、有数据、分页)
|
||||||
- [ ] 5.2.2 测试多条件筛选(名称、系列、状态、上架状态、类型)
|
- [ ] 5.2.2 测试多条件筛选(名称、系列、状态、上架状态、类型)
|
||||||
- [ ] 5.2.3 测试系列下拉选择器(只显示启用状态的系列)
|
- [ ] 5.2.3 测试系列下拉选择器(只显示启用状态的系列)
|
||||||
@@ -110,6 +120,7 @@
|
|||||||
- [ ] 5.2.9 测试权限控制(未登录、无权限)
|
- [ ] 5.2.9 测试权限控制(未登录、无权限)
|
||||||
|
|
||||||
### 5.3 代理可售套餐测试
|
### 5.3 代理可售套餐测试
|
||||||
|
|
||||||
- [ ] 5.3.1 测试列表查询(空列表、有数据、分页)
|
- [ ] 5.3.1 测试列表查询(空列表、有数据、分页)
|
||||||
- [ ] 5.3.2 测试筛选功能(按系列、按类型)
|
- [ ] 5.3.2 测试筛选功能(按系列、按类型)
|
||||||
- [ ] 5.3.3 测试详情查询(显示成本价、建议售价、利润空间、价格来源)
|
- [ ] 5.3.3 测试详情查询(显示成本价、建议售价、利润空间、价格来源)
|
||||||
@@ -118,6 +129,7 @@
|
|||||||
- [ ] 5.3.6 测试权限控制(非代理商用户无法访问)
|
- [ ] 5.3.6 测试权限控制(非代理商用户无法访问)
|
||||||
|
|
||||||
### 5.4 单套餐分配测试
|
### 5.4 单套餐分配测试
|
||||||
|
|
||||||
- [ ] 5.4.1 测试列表查询(空列表、有数据、分页)
|
- [ ] 5.4.1 测试列表查询(空列表、有数据、分页)
|
||||||
- [ ] 5.4.2 测试筛选功能(按店铺、按套餐、按状态)
|
- [ ] 5.4.2 测试筛选功能(按店铺、按套餐、按状态)
|
||||||
- [ ] 5.4.3 测试套餐下拉选择器(只显示启用且上架的套餐)
|
- [ ] 5.4.3 测试套餐下拉选择器(只显示启用且上架的套餐)
|
||||||
@@ -130,6 +142,7 @@
|
|||||||
- [ ] 5.4.10 测试权限控制(仅管理员可操作)
|
- [ ] 5.4.10 测试权限控制(仅管理员可操作)
|
||||||
|
|
||||||
### 5.5 通用功能测试
|
### 5.5 通用功能测试
|
||||||
|
|
||||||
- [ ] 5.5.1 测试所有页面的表单验证(必填、长度、格式)
|
- [ ] 5.5.1 测试所有页面的表单验证(必填、长度、格式)
|
||||||
- [ ] 5.5.2 测试所有页面的 loading 状态(列表、提交、删除)
|
- [ ] 5.5.2 测试所有页面的 loading 状态(列表、提交、删除)
|
||||||
- [ ] 5.5.3 测试所有页面的错误处理(网络错误、业务错误)
|
- [ ] 5.5.3 测试所有页面的错误处理(网络错误、业务错误)
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
当前系统使用简单的定价模式(pricing_mode/pricing_value)和一次性佣金(one_time_commission_*)来管理套餐系列分配。随着业务发展,需要更复杂的佣金系统:
|
当前系统使用简单的定价模式(pricing*mode/pricing_value)和一次性佣金(one_time_commission*\*)来管理套餐系列分配。随着业务发展,需要更复杂的佣金系统:
|
||||||
|
|
||||||
- 支持基础返佣(固定金额或百分比)
|
- 支持基础返佣(固定金额或百分比)
|
||||||
- 支持梯度返佣(根据销量或销售额分档返佣)
|
- 支持梯度返佣(根据销量或销售额分档返佣)
|
||||||
- 更清晰的数据模型和API接口
|
- 更清晰的数据模型和API接口
|
||||||
|
|
||||||
**背景约束**:
|
**背景约束**:
|
||||||
|
|
||||||
- 前后端需要同步部署(Breaking Change)
|
- 前后端需要同步部署(Breaking Change)
|
||||||
- 需要数据迁移方案
|
- 需要数据迁移方案
|
||||||
- 影响现有的套餐系列分配功能
|
- 影响现有的套餐系列分配功能
|
||||||
@@ -15,12 +17,14 @@
|
|||||||
## Goals / Non-Goals
|
## Goals / Non-Goals
|
||||||
|
|
||||||
**Goals**:
|
**Goals**:
|
||||||
|
|
||||||
- 实现新的佣金配置模型,支持基础返佣和梯度返佣
|
- 实现新的佣金配置模型,支持基础返佣和梯度返佣
|
||||||
- 提供清晰的UI界面让用户配置复杂的返佣规则
|
- 提供清晰的UI界面让用户配置复杂的返佣规则
|
||||||
- 保证数据一致性和类型安全
|
- 保证数据一致性和类型安全
|
||||||
- 提供良好的用户体验(表单验证、错误提示)
|
- 提供良好的用户体验(表单验证、错误提示)
|
||||||
|
|
||||||
**Non-Goals**:
|
**Non-Goals**:
|
||||||
|
|
||||||
- 不处理历史数据的完整性验证(由后端负责)
|
- 不处理历史数据的完整性验证(由后端负责)
|
||||||
- 不实现佣金计算逻辑(由后端负责)
|
- 不实现佣金计算逻辑(由后端负责)
|
||||||
- 不处理佣金结算流程(属于其他模块)
|
- 不处理佣金结算流程(属于其他模块)
|
||||||
@@ -32,11 +36,13 @@
|
|||||||
**决策**: 采用嵌套对象结构表示佣金配置
|
**决策**: 采用嵌套对象结构表示佣金配置
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- `base_commission: { mode, value }` - 清晰表达基础返佣的两个维度
|
- `base_commission: { mode, value }` - 清晰表达基础返佣的两个维度
|
||||||
- `tier_config: { period_type, tier_type, tiers[] }` - 梯度配置与基础配置分离,可选性强
|
- `tier_config: { period_type, tier_type, tiers[] }` - 梯度配置与基础配置分离,可选性强
|
||||||
- `tiers: [{ threshold, mode, value }]` - 每个档位都有独立的返佣模式和值
|
- `tiers: [{ threshold, mode, value }]` - 每个档位都有独立的返佣模式和值
|
||||||
|
|
||||||
**替代方案考虑**:
|
**替代方案考虑**:
|
||||||
|
|
||||||
- ❌ 平铺所有字段 - 会导致字段过多,语义不清晰
|
- ❌ 平铺所有字段 - 会导致字段过多,语义不清晰
|
||||||
- ❌ 使用JSON字符串存储配置 - 失去类型安全,不利于表单编辑
|
- ❌ 使用JSON字符串存储配置 - 失去类型安全,不利于表单编辑
|
||||||
|
|
||||||
@@ -45,6 +51,7 @@
|
|||||||
**决策**: 使用渐进式表单设计
|
**决策**: 使用渐进式表单设计
|
||||||
|
|
||||||
**表单结构**:
|
**表单结构**:
|
||||||
|
|
||||||
```
|
```
|
||||||
1. 基础返佣配置 (必填)
|
1. 基础返佣配置 (必填)
|
||||||
- 返佣模式: 单选 (固定金额/百分比)
|
- 返佣模式: 单选 (固定金额/百分比)
|
||||||
@@ -63,11 +70,13 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 渐进式设计降低初始复杂度
|
- 渐进式设计降低初始复杂度
|
||||||
- 只有启用梯度返佣时才显示相关配置
|
- 只有启用梯度返佣时才显示相关配置
|
||||||
- 动态档位列表提供灵活性
|
- 动态档位列表提供灵活性
|
||||||
|
|
||||||
**替代方案考虑**:
|
**替代方案考虑**:
|
||||||
|
|
||||||
- ❌ 全部平铺展示 - 对不需要梯度返佣的用户造成困扰
|
- ❌ 全部平铺展示 - 对不需要梯度返佣的用户造成困扰
|
||||||
- ❌ 使用向导模式 - 增加操作步骤,不适合编辑场景
|
- ❌ 使用向导模式 - 增加操作步骤,不适合编辑场景
|
||||||
|
|
||||||
@@ -76,7 +85,9 @@
|
|||||||
**决策**: 分层验证 + 条件验证
|
**决策**: 分层验证 + 条件验证
|
||||||
|
|
||||||
**验证规则**:
|
**验证规则**:
|
||||||
|
|
||||||
1. 基础返佣配置:
|
1. 基础返佣配置:
|
||||||
|
|
||||||
- mode: 必选
|
- mode: 必选
|
||||||
- value: 必填,>= 0
|
- value: 必填,>= 0
|
||||||
|
|
||||||
@@ -91,6 +102,7 @@
|
|||||||
- 档位阈值必须递增
|
- 档位阈值必须递增
|
||||||
|
|
||||||
**实现方式**:
|
**实现方式**:
|
||||||
|
|
||||||
- 使用 Element Plus 的表单验证
|
- 使用 Element Plus 的表单验证
|
||||||
- 自定义validator处理档位阈值递增验证
|
- 自定义validator处理档位阈值递增验证
|
||||||
- 使用 computed 动态生成验证规则
|
- 使用 computed 动态生成验证规则
|
||||||
@@ -100,11 +112,13 @@
|
|||||||
**决策**: 统一使用 `list` 字段名,添加适配层
|
**决策**: 统一使用 `list` 字段名,添加适配层
|
||||||
|
|
||||||
**理由**:
|
**理由**:
|
||||||
|
|
||||||
- 后端统一规范使用 `list` 而非 `items`
|
- 后端统一规范使用 `list` 而非 `items`
|
||||||
- 添加 `total_pages` 字段提供更完整的分页信息
|
- 添加 `total_pages` 字段提供更完整的分页信息
|
||||||
- 保持前端代码与后端规范一致
|
- 保持前端代码与后端规范一致
|
||||||
|
|
||||||
**迁移策略**:
|
**迁移策略**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
const res = await getShopSeriesAllocations(params)
|
const res = await getShopSeriesAllocations(params)
|
||||||
@@ -122,6 +136,7 @@ allocationList.value = res.data.list || []
|
|||||||
**风险**: 前后端必须同时部署,否则会出现接口不兼容
|
**风险**: 前后端必须同时部署,否则会出现接口不兼容
|
||||||
|
|
||||||
**缓解措施**:
|
**缓解措施**:
|
||||||
|
|
||||||
- 与后端团队协调部署窗口
|
- 与后端团队协调部署窗口
|
||||||
- 准备回退方案(前端代码分支)
|
- 准备回退方案(前端代码分支)
|
||||||
- 在测试环境充分验证
|
- 在测试环境充分验证
|
||||||
@@ -131,6 +146,7 @@ allocationList.value = res.data.list || []
|
|||||||
**风险**: 梯度返佣配置较复杂,用户可能不理解
|
**风险**: 梯度返佣配置较复杂,用户可能不理解
|
||||||
|
|
||||||
**缓解措施**:
|
**缓解措施**:
|
||||||
|
|
||||||
- 提供清晰的字段说明
|
- 提供清晰的字段说明
|
||||||
- 添加示例或帮助文档链接
|
- 添加示例或帮助文档链接
|
||||||
- 使用默认值简化初次配置
|
- 使用默认值简化初次配置
|
||||||
@@ -140,6 +156,7 @@ allocationList.value = res.data.list || []
|
|||||||
**风险**: 旧数据无法完全转换为新模型
|
**风险**: 旧数据无法完全转换为新模型
|
||||||
|
|
||||||
**缓解措施**:
|
**缓解措施**:
|
||||||
|
|
||||||
- 要求后端提供数据迁移脚本和验证
|
- 要求后端提供数据迁移脚本和验证
|
||||||
- 在迁移后检查数据完整性
|
- 在迁移后检查数据完整性
|
||||||
- 保留旧数据备份
|
- 保留旧数据备份
|
||||||
@@ -147,22 +164,26 @@ allocationList.value = res.data.list || []
|
|||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
### Phase 1: 准备阶段
|
### Phase 1: 准备阶段
|
||||||
|
|
||||||
1. 与后端确认API变更细节和时间表
|
1. 与后端确认API变更细节和时间表
|
||||||
2. 在开发环境实现前端变更
|
2. 在开发环境实现前端变更
|
||||||
3. 与后端在测试环境联调
|
3. 与后端在测试环境联调
|
||||||
|
|
||||||
### Phase 2: 测试阶段
|
### Phase 2: 测试阶段
|
||||||
|
|
||||||
1. 功能测试: 创建、编辑、删除、列表展示
|
1. 功能测试: 创建、编辑、删除、列表展示
|
||||||
2. 集成测试: 与后端API集成
|
2. 集成测试: 与后端API集成
|
||||||
3. 用户验收测试: 业务人员验证
|
3. 用户验收测试: 业务人员验证
|
||||||
|
|
||||||
### Phase 3: 部署阶段
|
### Phase 3: 部署阶段
|
||||||
|
|
||||||
1. 准备回退方案
|
1. 准备回退方案
|
||||||
2. 与后端协调部署窗口
|
2. 与后端协调部署窗口
|
||||||
3. 同步部署前后端
|
3. 同步部署前后端
|
||||||
4. 验证生产环境功能
|
4. 验证生产环境功能
|
||||||
|
|
||||||
### Phase 4: 监控阶段
|
### Phase 4: 监控阶段
|
||||||
|
|
||||||
1. 监控API错误率
|
1. 监控API错误率
|
||||||
2. 收集用户反馈
|
2. 收集用户反馈
|
||||||
3. 修复遗留问题
|
3. 修复遗留问题
|
||||||
@@ -170,6 +191,7 @@ allocationList.value = res.data.list || []
|
|||||||
### Rollback Plan
|
### Rollback Plan
|
||||||
|
|
||||||
如果部署后发现严重问题:
|
如果部署后发现严重问题:
|
||||||
|
|
||||||
1. 前端回退到上一版本
|
1. 前端回退到上一版本
|
||||||
2. 后端回退API(如果可能)
|
2. 后端回退API(如果可能)
|
||||||
3. 通知用户暂时不可用
|
3. 通知用户暂时不可用
|
||||||
@@ -178,12 +200,15 @@ allocationList.value = res.data.list || []
|
|||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
1. **Q**: 梯度返佣的档位数量是否有上限?
|
1. **Q**: 梯度返佣的档位数量是否有上限?
|
||||||
|
|
||||||
- **A**: 待后端确认,前端可以先不限制或设置合理上限(如10个)
|
- **A**: 待后端确认,前端可以先不限制或设置合理上限(如10个)
|
||||||
|
|
||||||
2. **Q**: 返佣值的单位和精度如何处理?
|
2. **Q**: 返佣值的单位和精度如何处理?
|
||||||
|
|
||||||
- **A**: 固定金额使用"分"为单位,百分比使用千分比(如200=20%),待后端确认
|
- **A**: 固定金额使用"分"为单位,百分比使用千分比(如200=20%),待后端确认
|
||||||
|
|
||||||
3. **Q**: 是否需要在列表页显示梯度返佣信息?
|
3. **Q**: 是否需要在列表页显示梯度返佣信息?
|
||||||
|
|
||||||
- **A**: 暂时只显示基础返佣,梯度信息在详情或编辑时查看
|
- **A**: 暂时只显示基础返佣,梯度信息在详情或编辑时查看
|
||||||
|
|
||||||
4. **Q**: 旧数据如何映射到新模型?
|
4. **Q**: 旧数据如何映射到新模型?
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Why
|
## Why
|
||||||
|
|
||||||
当前套餐系列分配使用简单的定价模式(固定加价/百分比加价)和一次性佣金配置,不支持复杂的返佣规则。新的业务需求要求支持:
|
当前套餐系列分配使用简单的定价模式(固定加价/百分比加价)和一次性佣金配置,不支持复杂的返佣规则。新的业务需求要求支持:
|
||||||
|
|
||||||
- 基础返佣配置(固定金额或百分比)
|
- 基础返佣配置(固定金额或百分比)
|
||||||
- 梯度返佣系统(根据销量或销售额分档返佣)
|
- 梯度返佣系统(根据销量或销售额分档返佣)
|
||||||
- 更灵活的佣金计算模型
|
- 更灵活的佣金计算模型
|
||||||
@@ -32,12 +33,14 @@
|
|||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
||||||
1. **API响应结构变更**:
|
1. **API响应结构变更**:
|
||||||
|
|
||||||
- 列表接口响应从 `{ items, page, page_size, total }` 改为 `{ list, page, page_size, total, total_pages }`
|
- 列表接口响应从 `{ items, page, page_size, total }` 改为 `{ list, page, page_size, total, total_pages }`
|
||||||
- 移除 `pricing_mode`, `pricing_value`, `calculated_cost_price` 字段
|
- 移除 `pricing_mode`, `pricing_value`, `calculated_cost_price` 字段
|
||||||
- 移除一次性佣金相关字段
|
- 移除一次性佣金相关字段
|
||||||
- 新增 `base_commission`, `enable_tier_commission` 字段
|
- 新增 `base_commission`, `enable_tier_commission` 字段
|
||||||
|
|
||||||
2. **API请求结构变更**:
|
2. **API请求结构变更**:
|
||||||
|
|
||||||
- 创建/更新接口需要新的 `base_commission` 对象
|
- 创建/更新接口需要新的 `base_commission` 对象
|
||||||
- 支持可选的 `enable_tier_commission` 和 `tier_config`
|
- 支持可选的 `enable_tier_commission` 和 `tier_config`
|
||||||
|
|
||||||
|
|||||||
@@ -129,15 +129,17 @@
|
|||||||
**Reason**: 旧的定价模式 (pricing_mode, pricing_value) 已被新的佣金模型替代
|
**Reason**: 旧的定价模式 (pricing_mode, pricing_value) 已被新的佣金模型替代
|
||||||
|
|
||||||
**Migration**: 旧数据通过后端迁移脚本转换为基础返佣配置:
|
**Migration**: 旧数据通过后端迁移脚本转换为基础返佣配置:
|
||||||
|
|
||||||
- pricing_mode="fixed" → base_commission.mode="fixed"
|
- pricing_mode="fixed" → base_commission.mode="fixed"
|
||||||
- pricing_mode="percentage" → base_commission.mode="percent"
|
- pricing_mode="percentage" → base_commission.mode="percent"
|
||||||
- pricing_value → base_commission.value (需要单位转换)
|
- pricing_value → base_commission.value (需要单位转换)
|
||||||
|
|
||||||
### Requirement: 一次性佣金配置
|
### Requirement: 一次性佣金配置
|
||||||
|
|
||||||
**Reason**: 一次性佣金配置 (one_time_commission_*) 已被梯度返佣系统替代
|
**Reason**: 一次性佣金配置 (one*time_commission*\*) 已被梯度返佣系统替代
|
||||||
|
|
||||||
**Migration**: 旧的一次性佣金通过以下方式迁移:
|
**Migration**: 旧的一次性佣金通过以下方式迁移:
|
||||||
|
|
||||||
- 如果设置了一次性佣金,转换为单档位的梯度返佣
|
- 如果设置了一次性佣金,转换为单档位的梯度返佣
|
||||||
- trigger → tier_type mapping (first_activation → sales_count, cumulative_recharge → sales_amount)
|
- trigger → tier_type mapping (first_activation → sales_count, cumulative_recharge → sales_amount)
|
||||||
- threshold → tiers[0].threshold
|
- threshold → tiers[0].threshold
|
||||||
@@ -165,7 +167,7 @@
|
|||||||
|
|
||||||
- **WHEN** base_commission.mode = "percent"
|
- **WHEN** base_commission.mode = "percent"
|
||||||
- **THEN** base_commission.value 表示返佣百分比的千分比 (如200表示20%)
|
- **THEN** base_commission.value 表示返佣百分比的千分比 (如200表示20%)
|
||||||
- **AND** 每笔交易返佣 = 交易金额 * (value / 1000)
|
- **AND** 每笔交易返佣 = 交易金额 \* (value / 1000)
|
||||||
|
|
||||||
### Requirement: 梯度返佣配置
|
### Requirement: 梯度返佣配置
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,9 @@
|
|||||||
try {
|
try {
|
||||||
const res = await UserService.getUserInfo()
|
const res = await UserService.getUserInfo()
|
||||||
if (res.code === ApiStatus.success && res.data) {
|
if (res.code === ApiStatus.success && res.data) {
|
||||||
// API 返回的是 { user, permissions },我们需要保存 user
|
// API 返回的是 { user, permissions },我们需要保存 user 和 permissions
|
||||||
userStore.setUserInfo(res.data.user)
|
userStore.setUserInfo(res.data.user)
|
||||||
|
userStore.setPermissions(res.data.permissions || [])
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户信息失败:', error)
|
console.error('获取用户信息失败:', error)
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ export class AuthorizationService extends BaseService {
|
|||||||
id: number,
|
id: number,
|
||||||
data: UpdateAuthorizationRemarkRequest
|
data: UpdateAuthorizationRemarkRequest
|
||||||
): Promise<BaseResponse<AuthorizationItem>> {
|
): Promise<BaseResponse<AuthorizationItem>> {
|
||||||
return this.put<BaseResponse<AuthorizationItem>>(
|
return this.put<BaseResponse<AuthorizationItem>>(`/api/admin/authorizations/${id}/remark`, data)
|
||||||
`/api/admin/authorizations/${id}/remark`,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ export class DeviceService extends BaseService {
|
|||||||
id: number,
|
id: number,
|
||||||
data: BindCardToDeviceRequest
|
data: BindCardToDeviceRequest
|
||||||
): Promise<BaseResponse<BindCardToDeviceResponse>> {
|
): Promise<BaseResponse<BindCardToDeviceResponse>> {
|
||||||
return this.post<BaseResponse<BindCardToDeviceResponse>>(
|
return this.post<BaseResponse<BindCardToDeviceResponse>>(`/api/admin/devices/${id}/cards`, data)
|
||||||
`/api/admin/devices/${id}/cards`,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,19 +103,14 @@ export class DeviceService extends BaseService {
|
|||||||
static allocateDevices(
|
static allocateDevices(
|
||||||
data: AllocateDevicesRequest
|
data: AllocateDevicesRequest
|
||||||
): Promise<BaseResponse<AllocateDevicesResponse>> {
|
): Promise<BaseResponse<AllocateDevicesResponse>> {
|
||||||
return this.post<BaseResponse<AllocateDevicesResponse>>(
|
return this.post<BaseResponse<AllocateDevicesResponse>>('/api/admin/devices/allocate', data)
|
||||||
'/api/admin/devices/allocate',
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量回收设备
|
* 批量回收设备
|
||||||
* @param data 回收参数
|
* @param data 回收参数
|
||||||
*/
|
*/
|
||||||
static recallDevices(
|
static recallDevices(data: RecallDevicesRequest): Promise<BaseResponse<RecallDevicesResponse>> {
|
||||||
data: RecallDevicesRequest
|
|
||||||
): Promise<BaseResponse<RecallDevicesResponse>> {
|
|
||||||
return this.post<BaseResponse<RecallDevicesResponse>>('/api/admin/devices/recall', data)
|
return this.post<BaseResponse<RecallDevicesResponse>>('/api/admin/devices/recall', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,9 +120,7 @@ export class DeviceService extends BaseService {
|
|||||||
* 批量导入设备
|
* 批量导入设备
|
||||||
* @param data 导入参数
|
* @param data 导入参数
|
||||||
*/
|
*/
|
||||||
static importDevices(
|
static importDevices(data: ImportDeviceRequest): Promise<BaseResponse<ImportDeviceResponse>> {
|
||||||
data: ImportDeviceRequest
|
|
||||||
): Promise<BaseResponse<ImportDeviceResponse>> {
|
|
||||||
return this.post<BaseResponse<ImportDeviceResponse>>('/api/admin/devices/import', data)
|
return this.post<BaseResponse<ImportDeviceResponse>>('/api/admin/devices/import', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,7 @@ export class EnterpriseService extends BaseService {
|
|||||||
* @param cardId 卡ID
|
* @param cardId 卡ID
|
||||||
*/
|
*/
|
||||||
static resumeCard(enterpriseId: number, cardId: number): Promise<BaseResponse> {
|
static resumeCard(enterpriseId: number, cardId: number): Promise<BaseResponse> {
|
||||||
return this.post<BaseResponse>(
|
return this.post<BaseResponse>(`/api/admin/enterprises/${enterpriseId}/cards/${cardId}/resume`)
|
||||||
`/api/admin/enterprises/${enterpriseId}/cards/${cardId}/resume`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,9 +144,7 @@ export class EnterpriseService extends BaseService {
|
|||||||
* @param cardId 卡ID
|
* @param cardId 卡ID
|
||||||
*/
|
*/
|
||||||
static suspendCard(enterpriseId: number, cardId: number): Promise<BaseResponse> {
|
static suspendCard(enterpriseId: number, cardId: number): Promise<BaseResponse> {
|
||||||
return this.post<BaseResponse>(
|
return this.post<BaseResponse>(`/api/admin/enterprises/${enterpriseId}/cards/${cardId}/suspend`)
|
||||||
`/api/admin/enterprises/${enterpriseId}/cards/${cardId}/suspend`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -87,5 +87,4 @@ export class PackageManageService extends BaseService {
|
|||||||
const data: UpdatePackageShelfStatusRequest = { shelf_status }
|
const data: UpdatePackageShelfStatusRequest = { shelf_status }
|
||||||
return this.patch<BaseResponse>(`/api/admin/packages/${id}/shelf`, data)
|
return this.patch<BaseResponse>(`/api/admin/packages/${id}/shelf`, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,10 +73,7 @@ export class PackageSeriesService extends BaseService {
|
|||||||
* @param id 系列ID
|
* @param id 系列ID
|
||||||
* @param status 状态 (1:启用, 2:禁用)
|
* @param status 状态 (1:启用, 2:禁用)
|
||||||
*/
|
*/
|
||||||
static updatePackageSeriesStatus(
|
static updatePackageSeriesStatus(id: number, status: number): Promise<BaseResponse> {
|
||||||
id: number,
|
|
||||||
status: number
|
|
||||||
): Promise<BaseResponse> {
|
|
||||||
const data: UpdatePackageSeriesStatusRequest = { status }
|
const data: UpdatePackageSeriesStatusRequest = { status }
|
||||||
return this.put<BaseResponse>(`/api/admin/package-series/${id}/status`, data)
|
return this.put<BaseResponse>(`/api/admin/package-series/${id}/status`, data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ export class ShopPackageAllocationService extends BaseService {
|
|||||||
static createShopPackageAllocation(
|
static createShopPackageAllocation(
|
||||||
data: CreateShopPackageAllocationRequest
|
data: CreateShopPackageAllocationRequest
|
||||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||||
return this.create<ShopPackageAllocationResponse>(
|
return this.create<ShopPackageAllocationResponse>('/api/admin/shop-package-allocations', data)
|
||||||
'/api/admin/shop-package-allocations',
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,9 +47,7 @@ export class ShopPackageAllocationService extends BaseService {
|
|||||||
static getShopPackageAllocationDetail(
|
static getShopPackageAllocationDetail(
|
||||||
id: number
|
id: number
|
||||||
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
): Promise<BaseResponse<ShopPackageAllocationResponse>> {
|
||||||
return this.getOne<ShopPackageAllocationResponse>(
|
return this.getOne<ShopPackageAllocationResponse>(`/api/admin/shop-package-allocations/${id}`)
|
||||||
`/api/admin/shop-package-allocations/${id}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export class ShopSeriesAllocationService extends BaseService {
|
|||||||
static getShopSeriesAllocations(
|
static getShopSeriesAllocations(
|
||||||
params?: ShopSeriesAllocationQueryParams
|
params?: ShopSeriesAllocationQueryParams
|
||||||
): Promise<PaginationResponse<ShopSeriesAllocationResponse>> {
|
): Promise<PaginationResponse<ShopSeriesAllocationResponse>> {
|
||||||
return this.getPage<ShopSeriesAllocationResponse>(
|
return this.getPage<ShopSeriesAllocationResponse>('/api/admin/shop-series-allocations', params)
|
||||||
'/api/admin/shop-series-allocations',
|
|
||||||
params
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
|
|
||||||
// 强制所有元素使用小米字体
|
// 强制所有元素使用小米字体
|
||||||
* {
|
* {
|
||||||
font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
|
font-family:
|
||||||
|
'MiSans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
// 按钮粗度
|
// 按钮粗度
|
||||||
--el-font-weight-primary: 400 !important;
|
--el-font-weight-primary: 400 !important;
|
||||||
// Element Plus 全局字体
|
// Element Plus 全局字体
|
||||||
--el-font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
|
--el-font-family:
|
||||||
|
'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
sans-serif !important;
|
||||||
|
|
||||||
--el-component-custom-height: 36px !important;
|
--el-component-custom-height: 36px !important;
|
||||||
|
|
||||||
@@ -182,7 +184,15 @@
|
|||||||
|
|
||||||
// 修改el-button样式
|
// 修改el-button样式
|
||||||
.el-button {
|
.el-button {
|
||||||
font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
|
font-family:
|
||||||
|
'MiSans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
sans-serif !important;
|
||||||
|
|
||||||
&.el-button--text {
|
&.el-button--text {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
@@ -202,7 +212,15 @@
|
|||||||
border-radius: 6px !important;
|
border-radius: 6px !important;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: all 0s !important;
|
transition: all 0s !important;
|
||||||
font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
|
font-family:
|
||||||
|
'MiSans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为所有 Element Plus 组件添加小米字体
|
// 为所有 Element Plus 组件添加小米字体
|
||||||
@@ -227,7 +245,15 @@
|
|||||||
.el-upload,
|
.el-upload,
|
||||||
.el-card,
|
.el-card,
|
||||||
.el-divider {
|
.el-divider {
|
||||||
font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
|
font-family:
|
||||||
|
'MiSans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-checkbox-group {
|
.el-checkbox-group {
|
||||||
|
|||||||
@@ -34,7 +34,15 @@ h5 {
|
|||||||
body {
|
body {
|
||||||
color: var(--art-text-gray-700);
|
color: var(--art-text-gray-700);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-family: 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family:
|
||||||
|
'MiSans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
BIN
src/composables/usePermission.ts
Normal file
BIN
src/composables/usePermission.ts
Normal file
Binary file not shown.
@@ -252,10 +252,7 @@ export const PRICE_SOURCE_MAP = PRICE_SOURCE_OPTIONS.reduce(
|
|||||||
map[item.value] = item
|
map[item.value] = item
|
||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
{} as Record<
|
{} as Record<PriceSource, { label: string; value: PriceSource; type: 'primary' | 'warning' }>
|
||||||
PriceSource,
|
|
||||||
{ label: string; value: PriceSource; type: 'primary' | 'warning' }
|
|
||||||
>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
import { router } from '@/router'
|
import { App, Directive, DirectiveBinding } from 'vue'
|
||||||
import { App, Directive } from 'vue'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限指令(后端控制模式可用)
|
* 权限指令
|
||||||
* 用法:
|
* 用法:
|
||||||
|
* v-permission="'menu:get'" - 单个权限
|
||||||
|
* v-permission="['menu:get', 'role:get']" - 多个权限(满足任意一个即可)
|
||||||
|
* v-permission:all="['menu:get', 'role:get']" - 多个权限(需要全部满足)
|
||||||
|
*
|
||||||
|
* 兼容旧的v-auth指令:
|
||||||
* <el-button v-auth="'add'">按钮</el-button>
|
* <el-button v-auth="'add'">按钮</el-button>
|
||||||
*/
|
*/
|
||||||
const authDirective: Directive = {
|
const permissionDirective: Directive = {
|
||||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
const authList = (router.currentRoute.value.meta.authList as Array<{ auth_mark: string }>) || []
|
const userStore = useUserStore()
|
||||||
|
const { value, arg } = binding
|
||||||
|
|
||||||
const hasPermission = authList.some((item) => item.auth_mark === binding.value)
|
// 如果没有值,直接返回
|
||||||
|
if (!value) return
|
||||||
|
|
||||||
|
let hasPermission = false
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 单个权限
|
||||||
|
hasPermission = userStore.hasPermission(value)
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
// 多个权限
|
||||||
|
if (arg === 'all') {
|
||||||
|
// 需要全部满足
|
||||||
|
hasPermission = userStore.hasAllPermissions(value)
|
||||||
|
} else {
|
||||||
|
// 满足任意一个即可
|
||||||
|
hasPermission = userStore.hasAnyPermission(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有权限,移除元素
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
el.parentNode?.removeChild(el)
|
el.parentNode?.removeChild(el)
|
||||||
}
|
}
|
||||||
@@ -19,5 +43,7 @@ const authDirective: Directive = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setupPermissionDirective(app: App) {
|
export function setupPermissionDirective(app: App) {
|
||||||
app.directive('auth', authDirective)
|
// 注册为 v-permission 和 v-auth (向后兼容)
|
||||||
|
app.directive('permission', permissionDirective)
|
||||||
|
app.directive('auth', permissionDirective)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,6 @@
|
|||||||
"standaloneCardList": "IoT卡管理",
|
"standaloneCardList": "IoT卡管理",
|
||||||
"iotCardTask": "IoT卡任务",
|
"iotCardTask": "IoT卡任务",
|
||||||
"deviceTask": "设备任务",
|
"deviceTask": "设备任务",
|
||||||
"taskDetail": "任务详情",
|
|
||||||
"devices": "设备管理",
|
"devices": "设备管理",
|
||||||
"deviceDetail": "设备详情",
|
"deviceDetail": "设备详情",
|
||||||
"assetAssign": "分配记录",
|
"assetAssign": "分配记录",
|
||||||
@@ -473,13 +472,6 @@
|
|||||||
"paymentMerchant": "支付商户",
|
"paymentMerchant": "支付商户",
|
||||||
"developerApi": "开发者API",
|
"developerApi": "开发者API",
|
||||||
"commissionTemplate": "分佣模板"
|
"commissionTemplate": "分佣模板"
|
||||||
},
|
|
||||||
"batch": {
|
|
||||||
"title": "批量操作",
|
|
||||||
"simImport": "网卡导入",
|
|
||||||
"deviceImport": "设备导入",
|
|
||||||
"offlineBatchRecharge": "线下批量充值",
|
|
||||||
"cardChangeNotice": "换卡通知"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -890,24 +890,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
icon: ''
|
icon: ''
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
path: 'iot-card-query',
|
|
||||||
name: 'CardSearch',
|
|
||||||
component: RoutesAlias.CardSearch,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.cardSearch',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'device-search',
|
|
||||||
name: 'DeviceSearch',
|
|
||||||
component: RoutesAlias.DeviceSearch,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.deviceSearch',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'iot-card-management',
|
path: 'iot-card-management',
|
||||||
name: 'StandaloneCardList',
|
name: 'StandaloneCardList',
|
||||||
@@ -935,16 +917,6 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'task-detail',
|
|
||||||
name: 'TaskDetail',
|
|
||||||
component: RoutesAlias.TaskDetail,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.assetManagement.taskDetail',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'devices',
|
path: 'devices',
|
||||||
name: 'DeviceList',
|
name: 'DeviceList',
|
||||||
@@ -1129,7 +1101,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
// {
|
// {
|
||||||
// path: '/settings',
|
// path: '/settings',
|
||||||
// name: 'Settings',
|
// name: 'Settings',
|
||||||
@@ -1167,52 +1139,5 @@ export const asyncRoutes: AppRouteRecord[] = [
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: '/batch',
|
|
||||||
name: 'Batch',
|
|
||||||
component: RoutesAlias.Home,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.batch.title',
|
|
||||||
icon: ''
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'sim-import',
|
|
||||||
name: 'SimImport',
|
|
||||||
component: RoutesAlias.SimImport,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.batch.simImport',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'device-import',
|
|
||||||
name: 'DeviceImport',
|
|
||||||
component: RoutesAlias.DeviceImport,
|
|
||||||
meta: {
|
|
||||||
title: 'menus.batch.deviceImport',
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// path: 'offline-batch-recharge',
|
|
||||||
// name: 'OfflineBatchRecharge',
|
|
||||||
// component: RoutesAlias.OfflineBatchRecharge,
|
|
||||||
// meta: {
|
|
||||||
// title: 'menus.batch.offlineBatchRecharge',
|
|
||||||
// keepAlive: true
|
|
||||||
// }
|
// }
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: 'card-change-notice',
|
|
||||||
// name: 'CardChangeNotice',
|
|
||||||
// component: RoutesAlias.CardChangeNotice,
|
|
||||||
// meta: {
|
|
||||||
// title: 'menus.batch.cardChangeNotice',
|
|
||||||
// keepAlive: true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -91,12 +91,9 @@ export enum RoutesAlias {
|
|||||||
SimCardAssign = '/product/sim-card-assign', // 号卡分配
|
SimCardAssign = '/product/sim-card-assign', // 号卡分配
|
||||||
|
|
||||||
// 资产管理
|
// 资产管理
|
||||||
CardSearch = '/asset-management/iot-card-query', // IoT卡查询
|
|
||||||
DeviceSearch = '/asset-management/device-search', // 设备查询
|
|
||||||
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
|
StandaloneCardList = '/asset-management/iot-card-management', // IoT卡管理
|
||||||
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
IotCardTask = '/asset-management/iot-card-task', // IoT卡任务
|
||||||
DeviceTask = '/asset-management/device-task', // 设备任务
|
DeviceTask = '/asset-management/device-task', // 设备任务
|
||||||
TaskDetail = '/asset-management/task-detail', // 任务详情
|
|
||||||
DeviceList = '/asset-management/device-list', // 设备列表
|
DeviceList = '/asset-management/device-list', // 设备列表
|
||||||
DeviceDetail = '/asset-management/device-detail', // 设备详情
|
DeviceDetail = '/asset-management/device-detail', // 设备详情
|
||||||
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
|
AssetAssign = '/asset-management/asset-assign', // 资产分配(分配记录)
|
||||||
@@ -121,12 +118,7 @@ export enum RoutesAlias {
|
|||||||
// 设置管理
|
// 设置管理
|
||||||
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
||||||
DeveloperApi = '/settings/developer-api', // 开发者API
|
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||||
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
CommissionTemplate = '/settings/commission-template' // 分佣模板
|
||||||
|
|
||||||
// 批量操作
|
|
||||||
SimImport = '/batch/sim-import', // 网卡批量导入
|
|
||||||
DeviceImport = '/batch/device-import', // 设备批量导入
|
|
||||||
CardChangeNotice = '/batch/card-change-notice' // 换卡通知
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主页路由
|
// 主页路由
|
||||||
|
|||||||
@@ -23,15 +23,45 @@ export const useUserStore = defineStore(
|
|||||||
const searchHistory = ref<AppRouteRecord[]>([])
|
const searchHistory = ref<AppRouteRecord[]>([])
|
||||||
const accessToken = ref('')
|
const accessToken = ref('')
|
||||||
const refreshToken = ref('')
|
const refreshToken = ref('')
|
||||||
|
const permissions = ref<string[]>([])
|
||||||
|
|
||||||
const getUserInfo = computed(() => info.value)
|
const getUserInfo = computed(() => info.value)
|
||||||
const getSettingState = computed(() => useSettingStore().$state)
|
const getSettingState = computed(() => useSettingStore().$state)
|
||||||
const getWorktabState = computed(() => useWorktabStore().$state)
|
const getWorktabState = computed(() => useWorktabStore().$state)
|
||||||
|
const getPermissions = computed(() => permissions.value)
|
||||||
|
|
||||||
const setUserInfo = (newInfo: UserInfo) => {
|
const setUserInfo = (newInfo: UserInfo) => {
|
||||||
info.value = newInfo
|
info.value = newInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setPermissions = (perms: string[]) => {
|
||||||
|
permissions.value = perms
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是超级管理员
|
||||||
|
const isSuperAdmin = computed(() => info.value.user_type === 1)
|
||||||
|
|
||||||
|
// 检查是否有某个权限
|
||||||
|
const hasPermission = (permission: string): boolean => {
|
||||||
|
// 超级管理员拥有所有权限
|
||||||
|
if (isSuperAdmin.value) return true
|
||||||
|
return permissions.value.includes(permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有某些权限中的任意一个
|
||||||
|
const hasAnyPermission = (perms: string[]): boolean => {
|
||||||
|
// 超级管理员拥有所有权限
|
||||||
|
if (isSuperAdmin.value) return true
|
||||||
|
return perms.some((perm) => permissions.value.includes(perm))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有所有指定权限
|
||||||
|
const hasAllPermissions = (perms: string[]): boolean => {
|
||||||
|
// 超级管理员拥有所有权限
|
||||||
|
if (isSuperAdmin.value) return true
|
||||||
|
return perms.every((perm) => permissions.value.includes(perm))
|
||||||
|
}
|
||||||
|
|
||||||
const setLoginStatus = (status: boolean) => {
|
const setLoginStatus = (status: boolean) => {
|
||||||
isLogin.value = status
|
isLogin.value = status
|
||||||
}
|
}
|
||||||
@@ -74,6 +104,7 @@ export const useUserStore = defineStore(
|
|||||||
lockPassword.value = ''
|
lockPassword.value = ''
|
||||||
accessToken.value = ''
|
accessToken.value = ''
|
||||||
refreshToken.value = ''
|
refreshToken.value = ''
|
||||||
|
permissions.value = []
|
||||||
useWorktabStore().opened = []
|
useWorktabStore().opened = []
|
||||||
sessionStorage.removeItem('iframeRoutes')
|
sessionStorage.removeItem('iframeRoutes')
|
||||||
resetRouterState(router)
|
resetRouterState(router)
|
||||||
@@ -90,10 +121,17 @@ export const useUserStore = defineStore(
|
|||||||
searchHistory,
|
searchHistory,
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
permissions,
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
getSettingState,
|
getSettingState,
|
||||||
getWorktabState,
|
getWorktabState,
|
||||||
|
getPermissions,
|
||||||
|
isSuperAdmin,
|
||||||
setUserInfo,
|
setUserInfo,
|
||||||
|
setPermissions,
|
||||||
|
hasPermission,
|
||||||
|
hasAnyPermission,
|
||||||
|
hasAllPermissions,
|
||||||
setLoginStatus,
|
setLoginStatus,
|
||||||
setLanguage,
|
setLanguage,
|
||||||
setSearchHistory,
|
setSearchHistory,
|
||||||
|
|||||||
607
src/types/auto-imports.d.ts
vendored
607
src/types/auto-imports.d.ts
vendored
@@ -6,7 +6,7 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: (typeof import('vue'))['EffectScope']
|
||||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||||
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
||||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||||
@@ -14,304 +14,319 @@ declare global {
|
|||||||
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
||||||
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
||||||
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
|
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
|
||||||
const ElTag: typeof import('element-plus/es')['ElTag']
|
const ElTag: (typeof import('element-plus/es'))['ElTag']
|
||||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
const acceptHMRUpdate: (typeof import('pinia'))['acceptHMRUpdate']
|
||||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
const asyncComputed: (typeof import('@vueuse/core'))['asyncComputed']
|
||||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
const autoResetRef: (typeof import('@vueuse/core'))['autoResetRef']
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: (typeof import('vue'))['computed']
|
||||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
const computedAsync: (typeof import('@vueuse/core'))['computedAsync']
|
||||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
const computedEager: (typeof import('@vueuse/core'))['computedEager']
|
||||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
const computedInject: (typeof import('@vueuse/core'))['computedInject']
|
||||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
const computedWithControl: (typeof import('@vueuse/core'))['computedWithControl']
|
||||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
const controlledComputed: (typeof import('@vueuse/core'))['controlledComputed']
|
||||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
const controlledRef: (typeof import('@vueuse/core'))['controlledRef']
|
||||||
const createApp: typeof import('vue')['createApp']
|
const createApp: (typeof import('vue'))['createApp']
|
||||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
const createEventHook: (typeof import('@vueuse/core'))['createEventHook']
|
||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
const createGlobalState: (typeof import('@vueuse/core'))['createGlobalState']
|
||||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
const createInjectionState: (typeof import('@vueuse/core'))['createInjectionState']
|
||||||
const createPinia: typeof import('pinia')['createPinia']
|
const createPinia: (typeof import('pinia'))['createPinia']
|
||||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
const createReactiveFn: (typeof import('@vueuse/core'))['createReactiveFn']
|
||||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
const createReusableTemplate: (typeof import('@vueuse/core'))['createReusableTemplate']
|
||||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
const createSharedComposable: (typeof import('@vueuse/core'))['createSharedComposable']
|
||||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
const createTemplatePromise: (typeof import('@vueuse/core'))['createTemplatePromise']
|
||||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
const createUnrefFn: (typeof import('@vueuse/core'))['createUnrefFn']
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: (typeof import('vue'))['customRef']
|
||||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
const debouncedRef: (typeof import('@vueuse/core'))['debouncedRef']
|
||||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
const debouncedWatch: (typeof import('@vueuse/core'))['debouncedWatch']
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: (typeof import('vue'))['defineComponent']
|
||||||
const defineStore: typeof import('pinia')['defineStore']
|
const defineStore: (typeof import('pinia'))['defineStore']
|
||||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
const eagerComputed: (typeof import('@vueuse/core'))['eagerComputed']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: (typeof import('vue'))['effectScope']
|
||||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
const extendRef: (typeof import('@vueuse/core'))['extendRef']
|
||||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
const getActivePinia: (typeof import('pinia'))['getActivePinia']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
||||||
const h: typeof import('vue')['h']
|
const h: (typeof import('vue'))['h']
|
||||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
const ignorableWatch: (typeof import('@vueuse/core'))['ignorableWatch']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: (typeof import('vue'))['inject']
|
||||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
const injectLocal: (typeof import('@vueuse/core'))['injectLocal']
|
||||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
const isDefined: (typeof import('@vueuse/core'))['isDefined']
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: (typeof import('vue'))['isProxy']
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: (typeof import('vue'))['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: (typeof import('vue'))['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: (typeof import('vue'))['isRef']
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
const makeDestructurable: (typeof import('@vueuse/core'))['makeDestructurable']
|
||||||
const mapActions: typeof import('pinia')['mapActions']
|
const mapActions: (typeof import('pinia'))['mapActions']
|
||||||
const mapGetters: typeof import('pinia')['mapGetters']
|
const mapGetters: (typeof import('pinia'))['mapGetters']
|
||||||
const mapState: typeof import('pinia')['mapState']
|
const mapState: (typeof import('pinia'))['mapState']
|
||||||
const mapStores: typeof import('pinia')['mapStores']
|
const mapStores: (typeof import('pinia'))['mapStores']
|
||||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
const mapWritableState: (typeof import('pinia'))['mapWritableState']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: (typeof import('vue'))['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: (typeof import('vue'))['nextTick']
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: (typeof import('vue'))['onActivated']
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
||||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave']
|
||||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate']
|
||||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
||||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
const onClickOutside: (typeof import('@vueuse/core'))['onClickOutside']
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
||||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
const onKeyStroke: (typeof import('@vueuse/core'))['onKeyStroke']
|
||||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
const onLongPress: (typeof import('@vueuse/core'))['onLongPress']
|
||||||
const onMounted: typeof import('vue')['onMounted']
|
const onMounted: (typeof import('vue'))['onMounted']
|
||||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
||||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
||||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
||||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
||||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
const onStartTyping: (typeof import('@vueuse/core'))['onStartTyping']
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: (typeof import('vue'))['onUpdated']
|
||||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
|
||||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
const pausableWatch: (typeof import('@vueuse/core'))['pausableWatch']
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: (typeof import('vue'))['provide']
|
||||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
const provideLocal: (typeof import('@vueuse/core'))['provideLocal']
|
||||||
const reactify: typeof import('@vueuse/core')['reactify']
|
const reactify: (typeof import('@vueuse/core'))['reactify']
|
||||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
const reactifyObject: (typeof import('@vueuse/core'))['reactifyObject']
|
||||||
const reactive: typeof import('vue')['reactive']
|
const reactive: (typeof import('vue'))['reactive']
|
||||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
const reactiveComputed: (typeof import('@vueuse/core'))['reactiveComputed']
|
||||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
const reactiveOmit: (typeof import('@vueuse/core'))['reactiveOmit']
|
||||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
const reactivePick: (typeof import('@vueuse/core'))['reactivePick']
|
||||||
const readonly: typeof import('vue')['readonly']
|
const readonly: (typeof import('vue'))['readonly']
|
||||||
const ref: typeof import('vue')['ref']
|
const ref: (typeof import('vue'))['ref']
|
||||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
const refAutoReset: (typeof import('@vueuse/core'))['refAutoReset']
|
||||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
const refDebounced: (typeof import('@vueuse/core'))['refDebounced']
|
||||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
const refDefault: (typeof import('@vueuse/core'))['refDefault']
|
||||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
const refThrottled: (typeof import('@vueuse/core'))['refThrottled']
|
||||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
const refWithControl: (typeof import('@vueuse/core'))['refWithControl']
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
||||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
const resolveRef: (typeof import('@vueuse/core'))['resolveRef']
|
||||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
const resolveUnref: (typeof import('@vueuse/core'))['resolveUnref']
|
||||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
const setActivePinia: (typeof import('pinia'))['setActivePinia']
|
||||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
const setMapStoreSuffix: (typeof import('pinia'))['setMapStoreSuffix']
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: (typeof import('vue'))['shallowRef']
|
||||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
const storeToRefs: (typeof import('pinia'))['storeToRefs']
|
||||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
const syncRef: (typeof import('@vueuse/core'))['syncRef']
|
||||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
const syncRefs: (typeof import('@vueuse/core'))['syncRefs']
|
||||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
const templateRef: (typeof import('@vueuse/core'))['templateRef']
|
||||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
const throttledRef: (typeof import('@vueuse/core'))['throttledRef']
|
||||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
const throttledWatch: (typeof import('@vueuse/core'))['throttledWatch']
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: (typeof import('vue'))['toRaw']
|
||||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
const toReactive: (typeof import('@vueuse/core'))['toReactive']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: (typeof import('vue'))['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: (typeof import('vue'))['toRefs']
|
||||||
const toValue: typeof import('vue')['toValue']
|
const toValue: (typeof import('vue'))['toValue']
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: (typeof import('vue'))['triggerRef']
|
||||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
const tryOnBeforeMount: (typeof import('@vueuse/core'))['tryOnBeforeMount']
|
||||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
const tryOnBeforeUnmount: (typeof import('@vueuse/core'))['tryOnBeforeUnmount']
|
||||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
const tryOnMounted: (typeof import('@vueuse/core'))['tryOnMounted']
|
||||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
const tryOnScopeDispose: (typeof import('@vueuse/core'))['tryOnScopeDispose']
|
||||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
const tryOnUnmounted: (typeof import('@vueuse/core'))['tryOnUnmounted']
|
||||||
const unref: typeof import('vue')['unref']
|
const unref: (typeof import('vue'))['unref']
|
||||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
const unrefElement: (typeof import('@vueuse/core'))['unrefElement']
|
||||||
const until: typeof import('@vueuse/core')['until']
|
const until: (typeof import('@vueuse/core'))['until']
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: (typeof import('@vueuse/core'))['useActiveElement']
|
||||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
const useAnimate: (typeof import('@vueuse/core'))['useAnimate']
|
||||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
const useArrayDifference: (typeof import('@vueuse/core'))['useArrayDifference']
|
||||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
const useArrayEvery: (typeof import('@vueuse/core'))['useArrayEvery']
|
||||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
const useArrayFilter: (typeof import('@vueuse/core'))['useArrayFilter']
|
||||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
const useArrayFind: (typeof import('@vueuse/core'))['useArrayFind']
|
||||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
const useArrayFindIndex: (typeof import('@vueuse/core'))['useArrayFindIndex']
|
||||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
const useArrayFindLast: (typeof import('@vueuse/core'))['useArrayFindLast']
|
||||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
const useArrayIncludes: (typeof import('@vueuse/core'))['useArrayIncludes']
|
||||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
const useArrayJoin: (typeof import('@vueuse/core'))['useArrayJoin']
|
||||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
const useArrayMap: (typeof import('@vueuse/core'))['useArrayMap']
|
||||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
const useArrayReduce: (typeof import('@vueuse/core'))['useArrayReduce']
|
||||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
const useArraySome: (typeof import('@vueuse/core'))['useArraySome']
|
||||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
const useArrayUnique: (typeof import('@vueuse/core'))['useArrayUnique']
|
||||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
const useAsyncQueue: (typeof import('@vueuse/core'))['useAsyncQueue']
|
||||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
const useAsyncState: (typeof import('@vueuse/core'))['useAsyncState']
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: (typeof import('vue'))['useAttrs']
|
||||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
const useBase64: (typeof import('@vueuse/core'))['useBase64']
|
||||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
const useBattery: (typeof import('@vueuse/core'))['useBattery']
|
||||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
const useBluetooth: (typeof import('@vueuse/core'))['useBluetooth']
|
||||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
const useBreakpoints: (typeof import('@vueuse/core'))['useBreakpoints']
|
||||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
const useBroadcastChannel: (typeof import('@vueuse/core'))['useBroadcastChannel']
|
||||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
const useBrowserLocation: (typeof import('@vueuse/core'))['useBrowserLocation']
|
||||||
const useCached: typeof import('@vueuse/core')['useCached']
|
const useCached: (typeof import('@vueuse/core'))['useCached']
|
||||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
const useClipboard: (typeof import('@vueuse/core'))['useClipboard']
|
||||||
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
const useClipboardItems: (typeof import('@vueuse/core'))['useClipboardItems']
|
||||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
const useCloned: (typeof import('@vueuse/core'))['useCloned']
|
||||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
const useColorMode: (typeof import('@vueuse/core'))['useColorMode']
|
||||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
const useConfirmDialog: (typeof import('@vueuse/core'))['useConfirmDialog']
|
||||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
const useCounter: (typeof import('@vueuse/core'))['useCounter']
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: (typeof import('vue'))['useCssModule']
|
||||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
const useCssVar: (typeof import('@vueuse/core'))['useCssVar']
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssVars: (typeof import('vue'))['useCssVars']
|
||||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
const useCurrentElement: (typeof import('@vueuse/core'))['useCurrentElement']
|
||||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
const useCycleList: (typeof import('@vueuse/core'))['useCycleList']
|
||||||
const useDark: typeof import('@vueuse/core')['useDark']
|
const useDark: (typeof import('@vueuse/core'))['useDark']
|
||||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
const useDateFormat: (typeof import('@vueuse/core'))['useDateFormat']
|
||||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
const useDebounce: (typeof import('@vueuse/core'))['useDebounce']
|
||||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
const useDebounceFn: (typeof import('@vueuse/core'))['useDebounceFn']
|
||||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
const useDebouncedRefHistory: (typeof import('@vueuse/core'))['useDebouncedRefHistory']
|
||||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
const useDeviceMotion: (typeof import('@vueuse/core'))['useDeviceMotion']
|
||||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
const useDeviceOrientation: (typeof import('@vueuse/core'))['useDeviceOrientation']
|
||||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
const useDevicePixelRatio: (typeof import('@vueuse/core'))['useDevicePixelRatio']
|
||||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
const useDevicesList: (typeof import('@vueuse/core'))['useDevicesList']
|
||||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
const useDisplayMedia: (typeof import('@vueuse/core'))['useDisplayMedia']
|
||||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
const useDocumentVisibility: (typeof import('@vueuse/core'))['useDocumentVisibility']
|
||||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
const useDraggable: (typeof import('@vueuse/core'))['useDraggable']
|
||||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
const useDropZone: (typeof import('@vueuse/core'))['useDropZone']
|
||||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
const useElementBounding: (typeof import('@vueuse/core'))['useElementBounding']
|
||||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
const useElementByPoint: (typeof import('@vueuse/core'))['useElementByPoint']
|
||||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
const useElementHover: (typeof import('@vueuse/core'))['useElementHover']
|
||||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
const useElementSize: (typeof import('@vueuse/core'))['useElementSize']
|
||||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
const useElementVisibility: (typeof import('@vueuse/core'))['useElementVisibility']
|
||||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
const useEventBus: (typeof import('@vueuse/core'))['useEventBus']
|
||||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
const useEventListener: (typeof import('@vueuse/core'))['useEventListener']
|
||||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
const useEventSource: (typeof import('@vueuse/core'))['useEventSource']
|
||||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
const useEyeDropper: (typeof import('@vueuse/core'))['useEyeDropper']
|
||||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
const useFavicon: (typeof import('@vueuse/core'))['useFavicon']
|
||||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
const useFetch: (typeof import('@vueuse/core'))['useFetch']
|
||||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
const useFileDialog: (typeof import('@vueuse/core'))['useFileDialog']
|
||||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
const useFileSystemAccess: (typeof import('@vueuse/core'))['useFileSystemAccess']
|
||||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
const useFocus: (typeof import('@vueuse/core'))['useFocus']
|
||||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
const useFocusWithin: (typeof import('@vueuse/core'))['useFocusWithin']
|
||||||
const useFps: typeof import('@vueuse/core')['useFps']
|
const useFps: (typeof import('@vueuse/core'))['useFps']
|
||||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
const useFullscreen: (typeof import('@vueuse/core'))['useFullscreen']
|
||||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
const useGamepad: (typeof import('@vueuse/core'))['useGamepad']
|
||||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
const useGeolocation: (typeof import('@vueuse/core'))['useGeolocation']
|
||||||
const useId: typeof import('vue')['useId']
|
const useId: (typeof import('vue'))['useId']
|
||||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
const useIdle: (typeof import('@vueuse/core'))['useIdle']
|
||||||
const useImage: typeof import('@vueuse/core')['useImage']
|
const useImage: (typeof import('@vueuse/core'))['useImage']
|
||||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
const useInfiniteScroll: (typeof import('@vueuse/core'))['useInfiniteScroll']
|
||||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
const useIntersectionObserver: (typeof import('@vueuse/core'))['useIntersectionObserver']
|
||||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
const useInterval: (typeof import('@vueuse/core'))['useInterval']
|
||||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
const useIntervalFn: (typeof import('@vueuse/core'))['useIntervalFn']
|
||||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
const useKeyModifier: (typeof import('@vueuse/core'))['useKeyModifier']
|
||||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
const useLastChanged: (typeof import('@vueuse/core'))['useLastChanged']
|
||||||
const useLink: typeof import('vue-router')['useLink']
|
const useLink: (typeof import('vue-router'))['useLink']
|
||||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
const useLocalStorage: (typeof import('@vueuse/core'))['useLocalStorage']
|
||||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
const useMagicKeys: (typeof import('@vueuse/core'))['useMagicKeys']
|
||||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
const useManualRefHistory: (typeof import('@vueuse/core'))['useManualRefHistory']
|
||||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
const useMediaControls: (typeof import('@vueuse/core'))['useMediaControls']
|
||||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
const useMediaQuery: (typeof import('@vueuse/core'))['useMediaQuery']
|
||||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
const useMemoize: (typeof import('@vueuse/core'))['useMemoize']
|
||||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
const useMemory: (typeof import('@vueuse/core'))['useMemory']
|
||||||
const useModel: typeof import('vue')['useModel']
|
const useModel: (typeof import('vue'))['useModel']
|
||||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
const useMounted: (typeof import('@vueuse/core'))['useMounted']
|
||||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
const useMouse: (typeof import('@vueuse/core'))['useMouse']
|
||||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
const useMouseInElement: (typeof import('@vueuse/core'))['useMouseInElement']
|
||||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
const useMousePressed: (typeof import('@vueuse/core'))['useMousePressed']
|
||||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
const useMutationObserver: (typeof import('@vueuse/core'))['useMutationObserver']
|
||||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
const useNavigatorLanguage: (typeof import('@vueuse/core'))['useNavigatorLanguage']
|
||||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
const useNetwork: (typeof import('@vueuse/core'))['useNetwork']
|
||||||
const useNow: typeof import('@vueuse/core')['useNow']
|
const useNow: (typeof import('@vueuse/core'))['useNow']
|
||||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
const useObjectUrl: (typeof import('@vueuse/core'))['useObjectUrl']
|
||||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
const useOffsetPagination: (typeof import('@vueuse/core'))['useOffsetPagination']
|
||||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
const useOnline: (typeof import('@vueuse/core'))['useOnline']
|
||||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
const usePageLeave: (typeof import('@vueuse/core'))['usePageLeave']
|
||||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
const useParallax: (typeof import('@vueuse/core'))['useParallax']
|
||||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
const useParentElement: (typeof import('@vueuse/core'))['useParentElement']
|
||||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
const usePerformanceObserver: (typeof import('@vueuse/core'))['usePerformanceObserver']
|
||||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
const usePermission: (typeof import('@vueuse/core'))['usePermission']
|
||||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
const usePointer: (typeof import('@vueuse/core'))['usePointer']
|
||||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
const usePointerLock: (typeof import('@vueuse/core'))['usePointerLock']
|
||||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
const usePointerSwipe: (typeof import('@vueuse/core'))['usePointerSwipe']
|
||||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
const usePreferredColorScheme: (typeof import('@vueuse/core'))['usePreferredColorScheme']
|
||||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
const usePreferredContrast: (typeof import('@vueuse/core'))['usePreferredContrast']
|
||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
const usePreferredDark: (typeof import('@vueuse/core'))['usePreferredDark']
|
||||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
const usePreferredLanguages: (typeof import('@vueuse/core'))['usePreferredLanguages']
|
||||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
const usePreferredReducedMotion: (typeof import('@vueuse/core'))['usePreferredReducedMotion']
|
||||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
const usePrevious: (typeof import('@vueuse/core'))['usePrevious']
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
const useRafFn: (typeof import('@vueuse/core'))['useRafFn']
|
||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
const useRefHistory: (typeof import('@vueuse/core'))['useRefHistory']
|
||||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
const useResizeObserver: (typeof import('@vueuse/core'))['useResizeObserver']
|
||||||
const useRoute: typeof import('vue-router')['useRoute']
|
const useRoute: (typeof import('vue-router'))['useRoute']
|
||||||
const useRouter: typeof import('vue-router')['useRouter']
|
const useRouter: (typeof import('vue-router'))['useRouter']
|
||||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
const useScreenOrientation: (typeof import('@vueuse/core'))['useScreenOrientation']
|
||||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
const useScreenSafeArea: (typeof import('@vueuse/core'))['useScreenSafeArea']
|
||||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
const useScriptTag: (typeof import('@vueuse/core'))['useScriptTag']
|
||||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
const useScroll: (typeof import('@vueuse/core'))['useScroll']
|
||||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
const useScrollLock: (typeof import('@vueuse/core'))['useScrollLock']
|
||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
const useSessionStorage: (typeof import('@vueuse/core'))['useSessionStorage']
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
const useShare: (typeof import('@vueuse/core'))['useShare']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: (typeof import('vue'))['useSlots']
|
||||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
const useSorted: (typeof import('@vueuse/core'))['useSorted']
|
||||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
const useSpeechRecognition: (typeof import('@vueuse/core'))['useSpeechRecognition']
|
||||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
const useSpeechSynthesis: (typeof import('@vueuse/core'))['useSpeechSynthesis']
|
||||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
const useStepper: (typeof import('@vueuse/core'))['useStepper']
|
||||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
const useStorage: (typeof import('@vueuse/core'))['useStorage']
|
||||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
const useStorageAsync: (typeof import('@vueuse/core'))['useStorageAsync']
|
||||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
const useStyleTag: (typeof import('@vueuse/core'))['useStyleTag']
|
||||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
const useSupported: (typeof import('@vueuse/core'))['useSupported']
|
||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
const useSwipe: (typeof import('@vueuse/core'))['useSwipe']
|
||||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
const useTemplateRefsList: (typeof import('@vueuse/core'))['useTemplateRefsList']
|
||||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
const useTextDirection: (typeof import('@vueuse/core'))['useTextDirection']
|
||||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
const useTextSelection: (typeof import('@vueuse/core'))['useTextSelection']
|
||||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
const useTextareaAutosize: (typeof import('@vueuse/core'))['useTextareaAutosize']
|
||||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
const useThrottle: (typeof import('@vueuse/core'))['useThrottle']
|
||||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
const useThrottleFn: (typeof import('@vueuse/core'))['useThrottleFn']
|
||||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
const useThrottledRefHistory: (typeof import('@vueuse/core'))['useThrottledRefHistory']
|
||||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
const useTimeAgo: (typeof import('@vueuse/core'))['useTimeAgo']
|
||||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
const useTimeout: (typeof import('@vueuse/core'))['useTimeout']
|
||||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
const useTimeoutFn: (typeof import('@vueuse/core'))['useTimeoutFn']
|
||||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
const useTimeoutPoll: (typeof import('@vueuse/core'))['useTimeoutPoll']
|
||||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
const useTimestamp: (typeof import('@vueuse/core'))['useTimestamp']
|
||||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
const useTitle: (typeof import('@vueuse/core'))['useTitle']
|
||||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
const useToNumber: (typeof import('@vueuse/core'))['useToNumber']
|
||||||
const useToString: typeof import('@vueuse/core')['useToString']
|
const useToString: (typeof import('@vueuse/core'))['useToString']
|
||||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
const useToggle: (typeof import('@vueuse/core'))['useToggle']
|
||||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
const useTransition: (typeof import('@vueuse/core'))['useTransition']
|
||||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
const useUrlSearchParams: (typeof import('@vueuse/core'))['useUrlSearchParams']
|
||||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
const useUserMedia: (typeof import('@vueuse/core'))['useUserMedia']
|
||||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
const useVModel: (typeof import('@vueuse/core'))['useVModel']
|
||||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
const useVModels: (typeof import('@vueuse/core'))['useVModels']
|
||||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
const useVibrate: (typeof import('@vueuse/core'))['useVibrate']
|
||||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
const useVirtualList: (typeof import('@vueuse/core'))['useVirtualList']
|
||||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
const useWakeLock: (typeof import('@vueuse/core'))['useWakeLock']
|
||||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
const useWebNotification: (typeof import('@vueuse/core'))['useWebNotification']
|
||||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
const useWebSocket: (typeof import('@vueuse/core'))['useWebSocket']
|
||||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
const useWebWorker: (typeof import('@vueuse/core'))['useWebWorker']
|
||||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
const useWebWorkerFn: (typeof import('@vueuse/core'))['useWebWorkerFn']
|
||||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
const useWindowFocus: (typeof import('@vueuse/core'))['useWindowFocus']
|
||||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
const useWindowScroll: (typeof import('@vueuse/core'))['useWindowScroll']
|
||||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
const useWindowSize: (typeof import('@vueuse/core'))['useWindowSize']
|
||||||
const watch: typeof import('vue')['watch']
|
const watch: (typeof import('vue'))['watch']
|
||||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
const watchArray: (typeof import('@vueuse/core'))['watchArray']
|
||||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
const watchAtMost: (typeof import('@vueuse/core'))['watchAtMost']
|
||||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
const watchDebounced: (typeof import('@vueuse/core'))['watchDebounced']
|
||||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
const watchDeep: (typeof import('@vueuse/core'))['watchDeep']
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: (typeof import('vue'))['watchEffect']
|
||||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
const watchIgnorable: (typeof import('@vueuse/core'))['watchIgnorable']
|
||||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
const watchImmediate: (typeof import('@vueuse/core'))['watchImmediate']
|
||||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
const watchOnce: (typeof import('@vueuse/core'))['watchOnce']
|
||||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
const watchPausable: (typeof import('@vueuse/core'))['watchPausable']
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
||||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
||||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
const watchThrottled: (typeof import('@vueuse/core'))['watchThrottled']
|
||||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
const watchTriggerable: (typeof import('@vueuse/core'))['watchTriggerable']
|
||||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
const watchWithFilter: (typeof import('@vueuse/core'))['watchWithFilter']
|
||||||
const whenever: typeof import('@vueuse/core')['whenever']
|
const whenever: (typeof import('@vueuse/core'))['whenever']
|
||||||
}
|
}
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type {
|
||||||
|
Component,
|
||||||
|
ComponentPublicInstance,
|
||||||
|
ComputedRef,
|
||||||
|
DirectiveBinding,
|
||||||
|
ExtractDefaultPropTypes,
|
||||||
|
ExtractPropTypes,
|
||||||
|
ExtractPublicPropTypes,
|
||||||
|
InjectionKey,
|
||||||
|
PropType,
|
||||||
|
Ref,
|
||||||
|
MaybeRef,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
VNode,
|
||||||
|
WritableComputedRef
|
||||||
|
} from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,9 @@
|
|||||||
|
|
||||||
<!-- 分配角色对话框 -->
|
<!-- 分配角色对话框 -->
|
||||||
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px">
|
||||||
<ElRadioGroup v-model="selectedRole" class="role-radio-group">
|
<ElCheckboxGroup v-model="selectedRoles">
|
||||||
<div v-for="role in allRoles" :key="role.ID" class="role-radio-item">
|
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px">
|
||||||
<ElRadio :label="role.ID">
|
<ElCheckbox :label="role.ID">
|
||||||
{{ role.role_name }}
|
{{ role.role_name }}
|
||||||
<ElTag
|
<ElTag
|
||||||
:type="role.role_type === 1 ? 'primary' : 'success'"
|
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||||
@@ -95,9 +95,9 @@
|
|||||||
>
|
>
|
||||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElRadio>
|
</ElCheckbox>
|
||||||
</div>
|
</div>
|
||||||
</ElRadioGroup>
|
</ElCheckboxGroup>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
<ElButton @click="roleDialogVisible = false">取消</ElButton>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { FormInstance, ElSwitch } from 'element-plus'
|
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const roleSubmitLoading = ref(false)
|
const roleSubmitLoading = ref(false)
|
||||||
const currentAccountId = ref<number>(0)
|
const currentAccountId = ref<number>(0)
|
||||||
const selectedRole = ref<number | undefined>(undefined)
|
const selectedRoles = ref<number[]>([])
|
||||||
const allRoles = ref<PlatformRole[]>([])
|
const allRoles = ref<PlatformRole[]>([])
|
||||||
|
|
||||||
// 定义表单搜索初始值
|
// 定义表单搜索初始值
|
||||||
@@ -292,7 +292,8 @@
|
|||||||
activeText: getStatusText(CommonStatus.ENABLED),
|
activeText: getStatusText(CommonStatus.ENABLED),
|
||||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -356,15 +357,18 @@
|
|||||||
// 显示分配角色对话框
|
// 显示分配角色对话框
|
||||||
const showRoleDialog = async (row: any) => {
|
const showRoleDialog = async (row: any) => {
|
||||||
currentAccountId.value = row.ID
|
currentAccountId.value = row.ID
|
||||||
selectedRole.value = undefined
|
selectedRoles.value = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 每次打开对话框时重新加载最新的角色列表
|
||||||
|
await loadAllRoles()
|
||||||
|
|
||||||
// 先加载当前账号的角色,再打开对话框
|
// 先加载当前账号的角色,再打开对话框
|
||||||
try {
|
|
||||||
const res = await AccountService.getAccountRoles(row.ID)
|
const res = await AccountService.getAccountRoles(row.ID)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
// 提取角色ID(只取第一个角色)
|
// 提取角色ID数组
|
||||||
const roles = res.data || []
|
const roles = res.data || []
|
||||||
selectedRole.value = roles.length > 0 ? roles[0].ID : undefined
|
selectedRoles.value = roles.map((role: any) => role.ID)
|
||||||
// 数据加载完成后再打开对话框
|
// 数据加载完成后再打开对话框
|
||||||
roleDialogVisible.value = true
|
roleDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -375,17 +379,13 @@
|
|||||||
|
|
||||||
// 提交分配角色
|
// 提交分配角色
|
||||||
const handleAssignRoles = async () => {
|
const handleAssignRoles = async () => {
|
||||||
if (selectedRole.value === undefined) {
|
|
||||||
ElMessage.warning('请选择一个角色')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
roleSubmitLoading.value = true
|
roleSubmitLoading.value = true
|
||||||
try {
|
try {
|
||||||
// 将单个角色ID包装成数组传给后端
|
await AccountService.assignRolesToAccount(currentAccountId.value, selectedRoles.value)
|
||||||
await AccountService.assignRolesToAccount(currentAccountId.value, [selectedRole.value])
|
|
||||||
ElMessage.success('分配角色成功')
|
ElMessage.success('分配角色成功')
|
||||||
roleDialogVisible.value = false
|
roleDialogVisible.value = false
|
||||||
|
// 刷新列表以更新角色显示
|
||||||
|
await getAccountList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -503,13 +503,4 @@
|
|||||||
.account-page {
|
.account-page {
|
||||||
// 账号管理页面样式
|
// 账号管理页面样式
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-radio-group {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-radio-item {
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -328,7 +328,8 @@
|
|||||||
activeText: '启用',
|
activeText: '启用',
|
||||||
inactiveText: '禁用',
|
inactiveText: '禁用',
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<ArtTableFullScreen>
|
<ArtTableFullScreen>
|
||||||
<div class="enterprise-cards-page" id="table-full-screen">
|
<div class="enterprise-cards-page" id="table-full-screen">
|
||||||
<!-- 企业信息卡片 -->
|
<!-- 企业信息卡片 -->
|
||||||
<ElCard shadow="never" style="margin-bottom: 16px">
|
<ElCard shadow="never" class="enterprise-info-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>企业信息</span>
|
<span>企业信息</span>
|
||||||
@@ -10,8 +10,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<ElDescriptions :column="3" border v-if="enterpriseInfo">
|
<ElDescriptions :column="3" border v-if="enterpriseInfo">
|
||||||
<ElDescriptionsItem label="企业名称">{{ enterpriseInfo.enterprise_name }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="企业名称">{{
|
||||||
<ElDescriptionsItem label="企业编号">{{ enterpriseInfo.enterprise_code }}</ElDescriptionsItem>
|
enterpriseInfo.enterprise_name
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="企业编号">{{
|
||||||
|
enterpriseInfo.enterprise_code
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="联系人">{{ enterpriseInfo.contact_name }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="联系人">{{ enterpriseInfo.contact_name }}</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
@@ -67,92 +71,51 @@
|
|||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="allocateDialogVisible"
|
v-model="allocateDialogVisible"
|
||||||
title="授权卡给企业"
|
title="授权卡给企业"
|
||||||
width="700px"
|
width="85%"
|
||||||
@close="handleAllocateDialogClose"
|
@close="handleAllocateDialogClose"
|
||||||
>
|
>
|
||||||
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
|
<!-- 搜索过滤条件 -->
|
||||||
<ElFormItem label="ICCID列表" prop="iccids">
|
<ArtSearchBar
|
||||||
<ElInput
|
v-model:filter="cardSearchForm"
|
||||||
v-model="iccidsText"
|
:items="cardSearchFormItems"
|
||||||
type="textarea"
|
label-width="85"
|
||||||
:rows="6"
|
@reset="handleCardSearchReset"
|
||||||
placeholder="请输入ICCID,每行一个或用逗号分隔"
|
@search="handleCardSearch"
|
||||||
@input="handleIccidsChange"
|
></ArtSearchBar>
|
||||||
|
|
||||||
|
<!-- 卡列表 -->
|
||||||
|
<div class="card-selection-info"> 已选择 {{ selectedAvailableCards.length }} 张卡 </div>
|
||||||
|
<ArtTable
|
||||||
|
ref="availableCardsTableRef"
|
||||||
|
row-key="id"
|
||||||
|
:loading="availableCardsLoading"
|
||||||
|
:data="availableCardsList"
|
||||||
|
:currentPage="cardPagination.page"
|
||||||
|
:pageSize="cardPagination.pageSize"
|
||||||
|
:total="cardPagination.total"
|
||||||
|
:marginTop="10"
|
||||||
|
@size-change="handleCardPageSizeChange"
|
||||||
|
@current-change="handleCardPageChange"
|
||||||
|
@selection-change="handleAvailableCardsSelectionChange"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElTableColumn type="selection" width="55" />
|
||||||
|
<ElTableColumn
|
||||||
|
v-for="col in availableCardColumns"
|
||||||
|
:key="col.prop || col.type"
|
||||||
|
v-bind="col"
|
||||||
/>
|
/>
|
||||||
<div style="color: var(--el-color-info); margin-top: 4px; font-size: 12px">
|
|
||||||
已输入 {{ allocateForm.iccids?.length || 0 }} 个ICCID
|
|
||||||
</div>
|
|
||||||
</ElFormItem>
|
|
||||||
<ElFormItem label="确认设备绑定">
|
|
||||||
<ElCheckbox v-model="allocateForm.confirm_device_bundles">
|
|
||||||
我确认已了解设备绑定关系,同意一起授权
|
|
||||||
</ElCheckbox>
|
|
||||||
</ElFormItem>
|
|
||||||
</ElForm>
|
|
||||||
|
|
||||||
<!-- 预检结果 -->
|
|
||||||
<div v-if="previewData" style="margin-top: 20px">
|
|
||||||
<ElDivider content-position="left">预检结果</ElDivider>
|
|
||||||
<ElDescriptions :column="2" border>
|
|
||||||
<ElDescriptionsItem label="待授权卡数">
|
|
||||||
{{ previewData.summary.total_cards }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="可授权卡数">
|
|
||||||
<ElTag type="success">{{ previewData.summary.valid_cards }}</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="独立卡数">
|
|
||||||
{{ previewData.summary.standalone_cards }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="设备绑定数">
|
|
||||||
<ElTag type="warning">{{ previewData.summary.device_bundles }}</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="失败数" :span="2">
|
|
||||||
<ElTag type="danger">{{ previewData.summary.failed_cards }}</ElTag>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
|
|
||||||
<!-- 失败项详情 -->
|
|
||||||
<div v-if="previewData.failed_items && previewData.failed_items.length > 0" style="margin-top: 16px">
|
|
||||||
<ElAlert title="以下ICCID无法授权" type="error" :closable="false" style="margin-bottom: 8px" />
|
|
||||||
<ElTable :data="previewData.failed_items" border max-height="200">
|
|
||||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
|
||||||
<ElTableColumn prop="reason" label="失败原因" />
|
|
||||||
</ElTable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 设备绑定详情 -->
|
|
||||||
<div v-if="previewData.device_bundles && previewData.device_bundles.length > 0" style="margin-top: 16px">
|
|
||||||
<ElAlert
|
|
||||||
title="以下ICCID与设备绑定,授权后设备也将一起授权给企业"
|
|
||||||
type="warning"
|
|
||||||
:closable="false"
|
|
||||||
style="margin-bottom: 8px"
|
|
||||||
/>
|
|
||||||
<ElTable :data="previewData.device_bundles" border max-height="200">
|
|
||||||
<ElTableColumn prop="device_imei" label="设备IMEI" width="180" />
|
|
||||||
<ElTableColumn label="绑定卡数">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.iccids?.length || 0 }}
|
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ArtTable>
|
||||||
<ElTableColumn label="ICCID列表">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.iccids?.join(', ') }}
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="allocateDialogVisible = false">取消</ElButton>
|
<ElButton @click="allocateDialogVisible = false">取消</ElButton>
|
||||||
<ElButton @click="handlePreview" :loading="previewLoading">预检</ElButton>
|
|
||||||
<ElButton
|
<ElButton
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleAllocate"
|
@click="handleAllocate"
|
||||||
:loading="allocateLoading"
|
:loading="allocateLoading"
|
||||||
:disabled="!previewData || previewData.summary.valid_cards === 0"
|
:disabled="selectedAvailableCards.length === 0"
|
||||||
>
|
>
|
||||||
确认授权
|
确认授权
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -191,7 +154,12 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 结果对话框 -->
|
<!-- 结果对话框 -->
|
||||||
<ElDialog v-model="resultDialogVisible" :title="resultTitle" width="700px">
|
<ElDialog
|
||||||
|
v-model="resultDialogVisible"
|
||||||
|
:title="resultTitle"
|
||||||
|
width="700px"
|
||||||
|
@close="handleResultDialogClose"
|
||||||
|
>
|
||||||
<ElDescriptions :column="2" border>
|
<ElDescriptions :column="2" border>
|
||||||
<ElDescriptionsItem label="成功数">
|
<ElDescriptionsItem label="成功数">
|
||||||
<ElTag type="success">{{ operationResult.success_count }}</ElTag>
|
<ElTag type="success">{{ operationResult.success_count }}</ElTag>
|
||||||
@@ -203,7 +171,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="operationResult.failed_items && operationResult.failed_items.length > 0"
|
v-if="operationResult.failed_items && operationResult.failed_items.length > 0"
|
||||||
style="margin-top: 20px"
|
class="result-section"
|
||||||
>
|
>
|
||||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||||
<ElTable :data="operationResult.failed_items" border max-height="300">
|
<ElTable :data="operationResult.failed_items" border max-height="300">
|
||||||
@@ -215,7 +183,7 @@
|
|||||||
<!-- 显示授权的设备 -->
|
<!-- 显示授权的设备 -->
|
||||||
<div
|
<div
|
||||||
v-if="operationResult.allocated_devices && operationResult.allocated_devices.length > 0"
|
v-if="operationResult.allocated_devices && operationResult.allocated_devices.length > 0"
|
||||||
style="margin-top: 20px"
|
class="result-section"
|
||||||
>
|
>
|
||||||
<ElDivider content-position="left">已授权设备</ElDivider>
|
<ElDivider content-position="left">已授权设备</ElDivider>
|
||||||
<ElTable :data="operationResult.allocated_devices" border max-height="200">
|
<ElTable :data="operationResult.allocated_devices" border max-height="200">
|
||||||
@@ -242,21 +210,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { EnterpriseService } from '@/api/modules'
|
import { EnterpriseService, CardService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import { BgColorEnum } from '@/enums/appEnum'
|
||||||
import type {
|
import type {
|
||||||
EnterpriseCardItem,
|
EnterpriseCardItem,
|
||||||
AllocateCardsPreviewResponse,
|
|
||||||
AllocateCardsResponse,
|
AllocateCardsResponse,
|
||||||
RecallCardsResponse,
|
RecallCardsResponse
|
||||||
FailedItem
|
|
||||||
} from '@/types/api/enterpriseCard'
|
} from '@/types/api/enterpriseCard'
|
||||||
import type { EnterpriseItem } from '@/types/api'
|
import type { EnterpriseItem } from '@/types/api'
|
||||||
|
import type { StandaloneIotCard } from '@/types/api/card'
|
||||||
|
|
||||||
defineOptions({ name: 'EnterpriseCards' })
|
defineOptions({ name: 'EnterpriseCards' })
|
||||||
|
|
||||||
@@ -265,19 +233,15 @@
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const allocateDialogVisible = ref(false)
|
const allocateDialogVisible = ref(false)
|
||||||
const allocateLoading = ref(false)
|
const allocateLoading = ref(false)
|
||||||
const previewLoading = ref(false)
|
|
||||||
const recallDialogVisible = ref(false)
|
const recallDialogVisible = ref(false)
|
||||||
const recallLoading = ref(false)
|
const recallLoading = ref(false)
|
||||||
const resultDialogVisible = ref(false)
|
const resultDialogVisible = ref(false)
|
||||||
const resultTitle = ref('')
|
const resultTitle = ref('')
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
const allocateFormRef = ref<FormInstance>()
|
|
||||||
const recallFormRef = ref<FormInstance>()
|
const recallFormRef = ref<FormInstance>()
|
||||||
const selectedCards = ref<EnterpriseCardItem[]>([])
|
const selectedCards = ref<EnterpriseCardItem[]>([])
|
||||||
const enterpriseId = ref<number>(0)
|
const enterpriseId = ref<number>(0)
|
||||||
const enterpriseInfo = ref<EnterpriseItem | null>(null)
|
const enterpriseInfo = ref<EnterpriseItem | null>(null)
|
||||||
const iccidsText = ref('')
|
|
||||||
const previewData = ref<AllocateCardsPreviewResponse | null>(null)
|
|
||||||
const operationResult = ref<AllocateCardsResponse | RecallCardsResponse>({
|
const operationResult = ref<AllocateCardsResponse | RecallCardsResponse>({
|
||||||
success_count: 0,
|
success_count: 0,
|
||||||
fail_count: 0,
|
fail_count: 0,
|
||||||
@@ -285,40 +249,39 @@
|
|||||||
allocated_devices: null
|
allocated_devices: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 可用卡列表相关
|
||||||
|
const availableCardsTableRef = ref()
|
||||||
|
const availableCardsLoading = ref(false)
|
||||||
|
const availableCardsList = ref<StandaloneIotCard[]>([])
|
||||||
|
const selectedAvailableCards = ref<StandaloneIotCard[]>([])
|
||||||
|
|
||||||
|
// 卡搜索表单初始值
|
||||||
|
const initialCardSearchState = {
|
||||||
|
status: undefined,
|
||||||
|
carrier_id: undefined,
|
||||||
|
iccid: '',
|
||||||
|
msisdn: '',
|
||||||
|
is_distributed: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardSearchForm = reactive({ ...initialCardSearchState })
|
||||||
|
const cardPagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
iccid: '',
|
iccid: '',
|
||||||
msisdn: '',
|
device_no: '',
|
||||||
status: undefined as number | undefined,
|
carrier_id: undefined as number | undefined,
|
||||||
authorization_status: undefined as number | undefined
|
status: undefined as number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = reactive({ ...initialSearchState })
|
const searchForm = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
// 授权表单
|
|
||||||
const allocateForm = reactive({
|
|
||||||
iccids: [] as string[],
|
|
||||||
confirm_device_bundles: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 授权表单验证规则
|
|
||||||
const allocateRules = reactive<FormRules>({
|
|
||||||
iccids: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
validator: (rule, value, callback) => {
|
|
||||||
if (!value || value.length === 0) {
|
|
||||||
callback(new Error('请输入至少一个ICCID'))
|
|
||||||
} else {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 回收表单
|
// 回收表单
|
||||||
const recallForm = reactive({
|
const recallForm = reactive({
|
||||||
reason: ''
|
reason: ''
|
||||||
@@ -346,16 +309,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '手机号',
|
label: '设备号',
|
||||||
prop: 'msisdn',
|
prop: 'device_no',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
config: {
|
config: {
|
||||||
clearable: true,
|
clearable: true,
|
||||||
placeholder: '请输入手机号'
|
placeholder: '请输入设备号'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '卡状态',
|
label: '运营商',
|
||||||
|
prop: 'carrier_id',
|
||||||
|
type: 'select',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '全部'
|
||||||
|
},
|
||||||
|
options: () => [
|
||||||
|
{ label: '中国移动', value: 1 },
|
||||||
|
{ label: '中国联通', value: 2 },
|
||||||
|
{ label: '中国电信', value: 3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
config: {
|
config: {
|
||||||
@@ -363,21 +340,74 @@
|
|||||||
placeholder: '全部'
|
placeholder: '全部'
|
||||||
},
|
},
|
||||||
options: () => [
|
options: () => [
|
||||||
{ label: '激活', value: 1 },
|
{ label: '在库', value: 1 },
|
||||||
{ label: '停机', value: 2 }
|
{ label: '已分销', value: 2 },
|
||||||
|
{ label: '已激活', value: 3 },
|
||||||
|
{ label: '已停用', value: 4 }
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 卡列表搜索表单配置
|
||||||
|
const cardSearchFormItems: SearchFormItem[] = [
|
||||||
{
|
{
|
||||||
label: '授权状态',
|
label: '状态',
|
||||||
prop: 'authorization_status',
|
prop: 'status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
config: {
|
config: {
|
||||||
clearable: true,
|
clearable: true,
|
||||||
placeholder: '全部'
|
placeholder: '全部'
|
||||||
},
|
},
|
||||||
options: () => [
|
options: () => [
|
||||||
{ label: '有效', value: 1 },
|
{ label: '在库', value: 1 },
|
||||||
{ label: '已回收', value: 0 }
|
{ label: '已分销', value: 2 },
|
||||||
|
{ label: '已激活', value: 3 },
|
||||||
|
{ label: '已停用', value: 4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '运营商',
|
||||||
|
prop: 'carrier_id',
|
||||||
|
type: 'select',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '全部'
|
||||||
|
},
|
||||||
|
options: () => [
|
||||||
|
{ label: '中国移动', value: 1 },
|
||||||
|
{ label: '中国联通', value: 2 },
|
||||||
|
{ label: '中国电信', value: 3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ICCID',
|
||||||
|
prop: 'iccid',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入ICCID'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '卡接入号',
|
||||||
|
prop: 'msisdn',
|
||||||
|
type: 'input',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入卡接入号'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '是否已分销',
|
||||||
|
prop: 'is_distributed',
|
||||||
|
type: 'select',
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '全部'
|
||||||
|
},
|
||||||
|
options: () => [
|
||||||
|
{ label: '是', value: true },
|
||||||
|
{ label: '否', value: false }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -385,12 +415,15 @@
|
|||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ICCID', prop: 'iccid' },
|
{ label: 'ICCID', prop: 'iccid' },
|
||||||
{ label: '手机号', prop: 'msisdn' },
|
{ label: '卡接入号', prop: 'msisdn' },
|
||||||
|
{ label: '设备号', prop: 'device_no' },
|
||||||
|
{ label: '运营商ID', prop: 'carrier_id' },
|
||||||
{ label: '运营商', prop: 'carrier_name' },
|
{ label: '运营商', prop: 'carrier_name' },
|
||||||
{ label: '卡状态', prop: 'status' },
|
{ label: '套餐名称', prop: 'package_name' },
|
||||||
{ label: '授权状态', prop: 'authorization_status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '授权时间', prop: 'authorized_at' },
|
{ label: '状态名称', prop: 'status_name' },
|
||||||
{ label: '授权人', prop: 'authorizer_name' },
|
{ label: '网络状态', prop: 'network_status' },
|
||||||
|
{ label: '网络状态名称', prop: 'network_status_name' },
|
||||||
{ label: '操作', prop: 'operation' }
|
{ label: '操作', prop: 'operation' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -398,22 +431,44 @@
|
|||||||
|
|
||||||
// 获取卡状态标签类型
|
// 获取卡状态标签类型
|
||||||
const getCardStatusTag = (status: number) => {
|
const getCardStatusTag = (status: number) => {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return 'info'
|
||||||
|
case 2:
|
||||||
|
return 'warning'
|
||||||
|
case 3:
|
||||||
|
return 'success'
|
||||||
|
case 4:
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取卡状态文本 - 使用API返回的status_name
|
||||||
|
const getCardStatusText = (status: number) => {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return '在库'
|
||||||
|
case 2:
|
||||||
|
return '已分销'
|
||||||
|
case 3:
|
||||||
|
return '已激活'
|
||||||
|
case 4:
|
||||||
|
return '已停用'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取网络状态标签类型
|
||||||
|
const getNetworkStatusTag = (status: number) => {
|
||||||
return status === 1 ? 'success' : 'danger'
|
return status === 1 ? 'success' : 'danger'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取卡状态文本
|
// 获取网络状态文本 - 使用API返回的network_status_name
|
||||||
const getCardStatusText = (status: number) => {
|
const getNetworkStatusText = (status: number) => {
|
||||||
return status === 1 ? '激活' : '停机'
|
return status === 1 ? '开机' : '停机'
|
||||||
}
|
|
||||||
|
|
||||||
// 获取授权状态标签类型
|
|
||||||
const getAuthStatusTag = (status: number) => {
|
|
||||||
return status === 1 ? 'success' : 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取授权状态文本
|
|
||||||
const getAuthStatusText = (status: number) => {
|
|
||||||
return status === 1 ? '有效' : '已回收'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
@@ -421,64 +476,77 @@
|
|||||||
{
|
{
|
||||||
prop: 'iccid',
|
prop: 'iccid',
|
||||||
label: 'ICCID',
|
label: 'ICCID',
|
||||||
minWidth: 180
|
minWidth: 200
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'msisdn',
|
prop: 'msisdn',
|
||||||
label: '手机号',
|
label: '卡接入号',
|
||||||
width: 120
|
width: 130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'device_no',
|
||||||
|
label: '设备号',
|
||||||
|
width: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'carrier_id',
|
||||||
|
label: '运营商ID',
|
||||||
|
width: 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'carrier_name',
|
prop: 'carrier_name',
|
||||||
label: '运营商',
|
label: '运营商',
|
||||||
width: 100
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'package_name',
|
||||||
|
label: '套餐名称',
|
||||||
|
width: 150
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '卡状态',
|
label: '状态',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: EnterpriseCardItem) => {
|
formatter: (row: EnterpriseCardItem) => {
|
||||||
return h(ElTag, { type: getCardStatusTag(row.status) }, () => getCardStatusText(row.status))
|
return h(ElTag, { type: getCardStatusTag(row.status) }, () => getCardStatusText(row.status))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'authorization_status',
|
prop: 'status_name',
|
||||||
label: '授权状态',
|
label: '状态名称',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'network_status',
|
||||||
|
label: '网络状态',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: EnterpriseCardItem) => {
|
formatter: (row: EnterpriseCardItem) => {
|
||||||
return h(
|
return h(ElTag, { type: getNetworkStatusTag(row.network_status) }, () =>
|
||||||
ElTag,
|
getNetworkStatusText(row.network_status)
|
||||||
{ type: getAuthStatusTag(row.authorization_status) },
|
|
||||||
() => getAuthStatusText(row.authorization_status)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'authorized_at',
|
prop: 'network_status_name',
|
||||||
label: '授权时间',
|
label: '网络状态名称',
|
||||||
width: 180,
|
width: 130
|
||||||
formatter: (row: EnterpriseCardItem) =>
|
|
||||||
row.authorized_at ? formatDateTime(row.authorized_at) : '-'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'authorizer_name',
|
|
||||||
label: '授权人',
|
|
||||||
width: 120
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 150,
|
width: 100,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: EnterpriseCardItem) => {
|
formatter: (row: EnterpriseCardItem) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
row.status === 2
|
row.network_status === 0
|
||||||
? h(ArtButtonTable, {
|
? h(ArtButtonTable, {
|
||||||
text: '复机',
|
text: '复机',
|
||||||
|
iconClass: BgColorEnum.SUCCESS,
|
||||||
onClick: () => handleResume(row)
|
onClick: () => handleResume(row)
|
||||||
})
|
})
|
||||||
: h(ArtButtonTable, {
|
: h(ArtButtonTable, {
|
||||||
text: '停机',
|
text: '停机',
|
||||||
|
iconClass: BgColorEnum.ERROR,
|
||||||
onClick: () => handleSuspend(row)
|
onClick: () => handleSuspend(row)
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
@@ -522,9 +590,9 @@
|
|||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
iccid: searchForm.iccid || undefined,
|
iccid: searchForm.iccid || undefined,
|
||||||
msisdn: searchForm.msisdn || undefined,
|
device_no: searchForm.device_no || undefined,
|
||||||
status: searchForm.status,
|
carrier_id: searchForm.carrier_id,
|
||||||
authorization_status: searchForm.authorization_status
|
status: searchForm.status
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理空值
|
// 清理空值
|
||||||
@@ -586,84 +654,213 @@
|
|||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示授权对话框
|
// 获取可用卡状态类型
|
||||||
const showAllocateDialog = () => {
|
const getAvailableCardStatusType = (status: number) => {
|
||||||
allocateDialogVisible.value = true
|
switch (status) {
|
||||||
iccidsText.value = ''
|
case 1:
|
||||||
allocateForm.iccids = []
|
return 'info'
|
||||||
allocateForm.confirm_device_bundles = false
|
case 2:
|
||||||
previewData.value = null
|
return 'warning'
|
||||||
if (allocateFormRef.value) {
|
case 3:
|
||||||
allocateFormRef.value.resetFields()
|
return 'success'
|
||||||
|
case 4:
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理ICCID输入变化
|
// 获取可用卡状态文本
|
||||||
const handleIccidsChange = () => {
|
const getAvailableCardStatusText = (status: number) => {
|
||||||
// 解析输入的ICCID,支持逗号、空格、换行分隔
|
switch (status) {
|
||||||
const iccids = iccidsText.value
|
case 1:
|
||||||
.split(/[,\s\n]+/)
|
return '在库'
|
||||||
.map((iccid) => iccid.trim())
|
case 2:
|
||||||
.filter((iccid) => iccid.length > 0)
|
return '已分销'
|
||||||
allocateForm.iccids = iccids
|
case 3:
|
||||||
// 清除预检结果
|
return '已激活'
|
||||||
previewData.value = null
|
case 4:
|
||||||
|
return '已停用'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预检
|
// 可用卡列表列配置
|
||||||
const handlePreview = async () => {
|
const availableCardColumns = computed(() => [
|
||||||
if (!allocateFormRef.value) return
|
{
|
||||||
|
prop: 'iccid',
|
||||||
|
label: 'ICCID',
|
||||||
|
minWidth: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'msisdn',
|
||||||
|
label: '卡接入号',
|
||||||
|
width: 130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'carrier_name',
|
||||||
|
label: '运营商',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'cost_price',
|
||||||
|
label: '成本价',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => `¥${(row.cost_price / 100).toFixed(2)}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'distribute_price',
|
||||||
|
label: '分销价',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => `¥${(row.distribute_price / 100).toFixed(2)}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
return h(ElTag, { type: getAvailableCardStatusType(row.status) }, () =>
|
||||||
|
getAvailableCardStatusText(row.status)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'activation_status',
|
||||||
|
label: '激活状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
const type = row.activation_status === 1 ? 'success' : 'info'
|
||||||
|
const text = row.activation_status === 1 ? '已激活' : '未激活'
|
||||||
|
return h(ElTag, { type }, () => text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'network_status',
|
||||||
|
label: '网络状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
const type = row.network_status === 1 ? 'success' : 'danger'
|
||||||
|
const text = row.network_status === 1 ? '开机' : '停机'
|
||||||
|
return h(ElTag, { type }, () => text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'real_name_status',
|
||||||
|
label: '实名状态',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
const type = row.real_name_status === 1 ? 'success' : 'warning'
|
||||||
|
const text = row.real_name_status === 1 ? '已实名' : '未实名'
|
||||||
|
return h(ElTag, { type }, () => text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'data_usage_mb',
|
||||||
|
label: '累计流量(MB)',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'first_commission_paid',
|
||||||
|
label: '首次佣金',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
const type = row.first_commission_paid ? 'success' : 'info'
|
||||||
|
const text = row.first_commission_paid ? '已支付' : '未支付'
|
||||||
|
return h(ElTag, { type, size: 'small' }, () => text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'accumulated_recharge',
|
||||||
|
label: '累计充值',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => `¥${(row.accumulated_recharge / 100).toFixed(2)}`
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
await allocateFormRef.value.validate(async (valid) => {
|
// 获取可用卡列表
|
||||||
if (valid) {
|
const getAvailableCardsList = async () => {
|
||||||
previewLoading.value = true
|
availableCardsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await EnterpriseService.previewAllocateCards(enterpriseId.value, {
|
const params: any = {
|
||||||
iccids: allocateForm.iccids
|
page: cardPagination.page,
|
||||||
|
page_size: cardPagination.pageSize,
|
||||||
|
...cardSearchForm
|
||||||
|
}
|
||||||
|
// 清理空值
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
if (params[key] === '' || params[key] === undefined) {
|
||||||
|
delete params[key]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const res = await CardService.getStandaloneIotCards(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
previewData.value = res.data
|
availableCardsList.value = res.data.items || []
|
||||||
ElMessage.success('预检完成')
|
cardPagination.total = res.data.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
ElMessage.error('预检失败')
|
ElMessage.error('获取卡列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
previewLoading.value = false
|
availableCardsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
// 处理卡列表选择变化
|
||||||
|
const handleAvailableCardsSelectionChange = (selection: StandaloneIotCard[]) => {
|
||||||
|
selectedAvailableCards.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡列表搜索
|
||||||
|
const handleCardSearch = () => {
|
||||||
|
cardPagination.page = 1
|
||||||
|
getAvailableCardsList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡列表重置
|
||||||
|
const handleCardSearchReset = () => {
|
||||||
|
Object.assign(cardSearchForm, { ...initialCardSearchState })
|
||||||
|
cardPagination.page = 1
|
||||||
|
getAvailableCardsList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡列表分页大小变化
|
||||||
|
const handleCardPageSizeChange = (newPageSize: number) => {
|
||||||
|
cardPagination.pageSize = newPageSize
|
||||||
|
getAvailableCardsList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡列表页码变化
|
||||||
|
const handleCardPageChange = (newPage: number) => {
|
||||||
|
cardPagination.page = newPage
|
||||||
|
getAvailableCardsList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示授权对话框
|
||||||
|
const showAllocateDialog = () => {
|
||||||
|
allocateDialogVisible.value = true
|
||||||
|
selectedAvailableCards.value = []
|
||||||
|
// 重置搜索条件
|
||||||
|
Object.assign(cardSearchForm, { ...initialCardSearchState })
|
||||||
|
cardPagination.page = 1
|
||||||
|
cardPagination.pageSize = 20
|
||||||
|
// 加载可用卡列表
|
||||||
|
getAvailableCardsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行授权
|
// 执行授权
|
||||||
const handleAllocate = async () => {
|
const handleAllocate = async () => {
|
||||||
if (!allocateFormRef.value) return
|
if (selectedAvailableCards.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要授权的卡')
|
||||||
if (!previewData.value) {
|
|
||||||
ElMessage.warning('请先进行预检')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewData.value.summary.valid_cards === 0) {
|
|
||||||
ElMessage.warning('没有可授权的卡')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有设备绑定且未确认,提示用户
|
|
||||||
if (
|
|
||||||
previewData.value.device_bundles &&
|
|
||||||
previewData.value.device_bundles.length > 0 &&
|
|
||||||
!allocateForm.confirm_device_bundles
|
|
||||||
) {
|
|
||||||
ElMessage.warning('请确认设备绑定关系')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allocateLoading.value = true
|
allocateLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
const iccids = selectedAvailableCards.value.map((card) => card.iccid)
|
||||||
const res = await EnterpriseService.allocateCards(enterpriseId.value, {
|
const res = await EnterpriseService.allocateCards(enterpriseId.value, {
|
||||||
iccids: allocateForm.iccids,
|
iccids
|
||||||
confirm_device_bundles: allocateForm.confirm_device_bundles || undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -683,10 +880,7 @@
|
|||||||
|
|
||||||
// 关闭授权对话框
|
// 关闭授权对话框
|
||||||
const handleAllocateDialogClose = () => {
|
const handleAllocateDialogClose = () => {
|
||||||
if (allocateFormRef.value) {
|
selectedAvailableCards.value = []
|
||||||
allocateFormRef.value.resetFields()
|
|
||||||
}
|
|
||||||
previewData.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示批量回收对话框
|
// 显示批量回收对话框
|
||||||
@@ -711,7 +905,7 @@
|
|||||||
recallLoading.value = true
|
recallLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await EnterpriseService.recallCards(enterpriseId.value, {
|
const res = await EnterpriseService.recallCards(enterpriseId.value, {
|
||||||
card_ids: selectedCards.value.map((card) => card.id)
|
iccids: selectedCards.value.map((card) => card.iccid)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -743,6 +937,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭结果对话框
|
||||||
|
const handleResultDialogClose = () => {
|
||||||
|
getTableData()
|
||||||
|
}
|
||||||
|
|
||||||
// 停机卡
|
// 停机卡
|
||||||
const handleSuspend = (row: EnterpriseCardItem) => {
|
const handleSuspend = (row: EnterpriseCardItem) => {
|
||||||
ElMessageBox.confirm('确定要停机该卡吗?', '停机卡', {
|
ElMessageBox.confirm('确定要停机该卡吗?', '停机卡', {
|
||||||
@@ -786,10 +985,34 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.enterprise-cards-page {
|
.enterprise-cards-page {
|
||||||
|
.enterprise-info-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-selection-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--el-color-info);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
|
import { BgColorEnum } from '@/enums/appEnum'
|
||||||
|
|
||||||
defineOptions({ name: 'EnterpriseCustomer' })
|
defineOptions({ name: 'EnterpriseCustomer' })
|
||||||
|
|
||||||
@@ -367,7 +368,8 @@
|
|||||||
{
|
{
|
||||||
prop: 'enterprise_code',
|
prop: 'enterprise_code',
|
||||||
label: '企业编号',
|
label: '企业编号',
|
||||||
minWidth: 150
|
minWidth: 150,
|
||||||
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'enterprise_name',
|
prop: 'enterprise_name',
|
||||||
@@ -423,7 +425,8 @@
|
|||||||
activeText: '启用',
|
activeText: '启用',
|
||||||
inactiveText: '禁用',
|
inactiveText: '禁用',
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -436,21 +439,24 @@
|
|||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 230,
|
width: 260,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: EnterpriseItem) => {
|
formatter: (row: EnterpriseItem) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
icon: '',
|
text: '编辑',
|
||||||
|
iconClass: BgColorEnum.SECONDARY,
|
||||||
|
onClick: () => showDialog('edit', row)
|
||||||
|
}),
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '卡授权',
|
||||||
|
iconClass: BgColorEnum.PRIMARY,
|
||||||
onClick: () => manageCards(row)
|
onClick: () => manageCards(row)
|
||||||
}),
|
}),
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
icon: '',
|
text: '修改密码',
|
||||||
|
iconClass: BgColorEnum.WARNING,
|
||||||
onClick: () => showPasswordDialog(row)
|
onClick: () => showPasswordDialog(row)
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'edit',
|
|
||||||
onClick: () => showDialog('edit', row)
|
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,7 +367,8 @@
|
|||||||
activeText: getStatusText(CommonStatus.ENABLED),
|
activeText: getStatusText(CommonStatus.ENABLED),
|
||||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -447,8 +448,11 @@
|
|||||||
currentAccountId.value = row.ID
|
currentAccountId.value = row.ID
|
||||||
selectedRoles.value = []
|
selectedRoles.value = []
|
||||||
|
|
||||||
// 先加载当前账号的角色,再打开对话框
|
|
||||||
try {
|
try {
|
||||||
|
// 每次打开对话框时重新加载最新的角色列表
|
||||||
|
await loadAllRoles()
|
||||||
|
|
||||||
|
// 先加载当前账号的角色,再打开对话框
|
||||||
const res = await PlatformAccountService.getPlatformAccountRoles(row.ID)
|
const res = await PlatformAccountService.getPlatformAccountRoles(row.ID)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
// 提取角色ID数组
|
// 提取角色ID数组
|
||||||
@@ -471,6 +475,8 @@
|
|||||||
})
|
})
|
||||||
ElMessage.success('分配角色成功')
|
ElMessage.success('分配角色成功')
|
||||||
roleDialogVisible.value = false
|
roleDialogVisible.value = false
|
||||||
|
// 刷新列表以更新角色显示
|
||||||
|
await getAccountList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -300,15 +300,15 @@
|
|||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
prop: 'id',
|
prop: 'id',
|
||||||
label: 'ID',
|
label: 'ID'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
label: '用户名',
|
label: '用户名'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
label: '手机号',
|
label: '手机号'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'shop_name',
|
prop: 'shop_name',
|
||||||
@@ -326,7 +326,8 @@
|
|||||||
activeText: getStatusText(CommonStatus.ENABLED),
|
activeText: getStatusText(CommonStatus.ENABLED),
|
||||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
<ElSkeleton :loading="loading" :rows="10" animated>
|
<ElSkeleton :loading="loading" :rows="10" animated>
|
||||||
<template #default>
|
<template #default>
|
||||||
<ElDescriptions v-if="recordDetail" title="基本信息" :column="3" border>
|
<ElDescriptions v-if="recordDetail" title="基本信息" :column="3" border>
|
||||||
<ElDescriptionsItem label="分配单号">{{ recordDetail.allocation_no }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="分配单号">{{
|
||||||
|
recordDetail.allocation_no
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="分配类型">
|
<ElDescriptionsItem label="分配类型">
|
||||||
<ElTag :type="getAllocationTypeType(recordDetail.allocation_type)">
|
<ElTag :type="getAllocationTypeType(recordDetail.allocation_type)">
|
||||||
{{ recordDetail.allocation_name }}
|
{{ recordDetail.allocation_name }}
|
||||||
@@ -23,14 +25,24 @@
|
|||||||
{{ recordDetail.asset_type_name }}
|
{{ recordDetail.asset_type_name }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="资产标识符">{{ recordDetail.asset_identifier }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="资产标识符">{{
|
||||||
<ElDescriptionsItem label="关联卡数量">{{ recordDetail.related_card_count }}</ElDescriptionsItem>
|
recordDetail.asset_identifier
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="关联卡数量">{{
|
||||||
|
recordDetail.related_card_count
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="关联设备ID">
|
<ElDescriptionsItem label="关联设备ID">
|
||||||
{{ recordDetail.related_device_id || '-' }}
|
{{ recordDetail.related_device_id || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<ElDescriptions v-if="recordDetail" title="所有者信息" :column="2" border style="margin-top: 20px">
|
<ElDescriptions
|
||||||
|
v-if="recordDetail"
|
||||||
|
title="所有者信息"
|
||||||
|
:column="2"
|
||||||
|
border
|
||||||
|
style="margin-top: 20px"
|
||||||
|
>
|
||||||
<ElDescriptionsItem label="来源所有者">
|
<ElDescriptionsItem label="来源所有者">
|
||||||
{{ recordDetail.from_owner_name }} ({{ recordDetail.from_owner_type }})
|
{{ recordDetail.from_owner_name }} ({{ recordDetail.from_owner_type }})
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
@@ -39,8 +51,16 @@
|
|||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<ElDescriptions v-if="recordDetail" title="操作信息" :column="2" border style="margin-top: 20px">
|
<ElDescriptions
|
||||||
<ElDescriptionsItem label="操作人">{{ recordDetail.operator_name }}</ElDescriptionsItem>
|
v-if="recordDetail"
|
||||||
|
title="操作信息"
|
||||||
|
:column="2"
|
||||||
|
border
|
||||||
|
style="margin-top: 20px"
|
||||||
|
>
|
||||||
|
<ElDescriptionsItem label="操作人">{{
|
||||||
|
recordDetail.operator_name
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="创建时间">
|
<ElDescriptionsItem label="创建时间">
|
||||||
{{ formatDateTime(recordDetail.created_at) }}
|
{{ formatDateTime(recordDetail.created_at) }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
@@ -50,7 +70,14 @@
|
|||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<!-- 关联卡列表 -->
|
<!-- 关联卡列表 -->
|
||||||
<div v-if="recordDetail && recordDetail.related_card_ids && recordDetail.related_card_ids.length > 0" style="margin-top: 20px">
|
<div
|
||||||
|
v-if="
|
||||||
|
recordDetail &&
|
||||||
|
recordDetail.related_card_ids &&
|
||||||
|
recordDetail.related_card_ids.length > 0
|
||||||
|
"
|
||||||
|
style="margin-top: 20px"
|
||||||
|
>
|
||||||
<ElDivider content-position="left">关联卡列表</ElDivider>
|
<ElDivider content-position="left">关联卡列表</ElDivider>
|
||||||
<ElTable :data="relatedCardsList" border>
|
<ElTable :data="relatedCardsList" border>
|
||||||
<ElTableColumn type="index" label="序号" width="60" />
|
<ElTableColumn type="index" label="序号" width="60" />
|
||||||
@@ -155,8 +182,8 @@
|
|||||||
.allocation-record-detail-page {
|
.allocation-record-detail-page {
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -53,11 +53,7 @@
|
|||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import type {
|
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
||||||
AssetAllocationRecord,
|
|
||||||
AllocationTypeEnum,
|
|
||||||
AssetTypeEnum
|
|
||||||
} from '@/types/api/card'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AssetAllocationRecords' })
|
defineOptions({ name: 'AssetAllocationRecords' })
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,9 @@
|
|||||||
{{ authorizationDetail.revoker_name || '-' }}
|
{{ authorizationDetail.revoker_name || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="回收时间">
|
<ElDescriptionsItem label="回收时间">
|
||||||
{{ authorizationDetail.revoked_at ? formatDateTime(authorizationDetail.revoked_at) : '-' }}
|
{{
|
||||||
|
authorizationDetail.revoked_at ? formatDateTime(authorizationDetail.revoked_at) : '-'
|
||||||
|
}}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="备注" :span="2">
|
<ElDescriptionsItem label="备注" :span="2">
|
||||||
@@ -109,8 +111,8 @@
|
|||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -240,10 +240,8 @@
|
|||||||
label: '授权人类型',
|
label: '授权人类型',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: AuthorizationItem) => {
|
formatter: (row: AuthorizationItem) => {
|
||||||
return h(
|
return h(ElTag, { type: getAuthorizerTypeTag(row.authorizer_type) }, () =>
|
||||||
ElTag,
|
getAuthorizerTypeText(row.authorizer_type)
|
||||||
{ type: getAuthorizerTypeTag(row.authorizer_type) },
|
|
||||||
() => getAuthorizerTypeText(row.authorizer_type)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<ElDescriptionsItem label="制造商">{{ deviceInfo.manufacturer }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="制造商">{{ deviceInfo.manufacturer }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="最大插槽数">{{ deviceInfo.max_sim_slots }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="最大插槽数">{{ deviceInfo.max_sim_slots }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="已绑定卡数">
|
<ElDescriptionsItem label="已绑定卡数">
|
||||||
<span style="color: #67c23a; font-weight: bold">{{ deviceInfo.bound_card_count }}</span>
|
<span style="font-weight: bold; color: #67c23a">{{ deviceInfo.bound_card_count }}</span>
|
||||||
/ {{ deviceInfo.max_sim_slots }}
|
/ {{ deviceInfo.max_sim_slots }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="所属店铺">
|
<ElDescriptionsItem label="所属店铺">
|
||||||
@@ -118,7 +118,11 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="插槽位置" prop="slot_position">
|
<ElFormItem label="插槽位置" prop="slot_position">
|
||||||
<ElSelect v-model="bindForm.slot_position" placeholder="请选择插槽位置" style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="bindForm.slot_position"
|
||||||
|
placeholder="请选择插槽位置"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="slot in availableSlots"
|
v-for="slot in availableSlots"
|
||||||
:key="slot"
|
:key="slot"
|
||||||
|
|||||||
@@ -56,9 +56,14 @@
|
|||||||
|
|
||||||
<!-- 批量分配对话框 -->
|
<!-- 批量分配对话框 -->
|
||||||
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
|
<ElDialog v-model="allocateDialogVisible" title="批量分配设备" width="600px">
|
||||||
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
|
<ElForm
|
||||||
|
ref="allocateFormRef"
|
||||||
|
:model="allocateForm"
|
||||||
|
:rules="allocateRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
<ElFormItem label="已选设备数">
|
<ElFormItem label="已选设备数">
|
||||||
<span style="color: #409eff; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> 台
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="目标店铺" prop="target_shop_id">
|
<ElFormItem label="目标店铺" prop="target_shop_id">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
@@ -93,7 +98,8 @@
|
|||||||
style="margin-bottom: 10px"
|
style="margin-bottom: 10px"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
成功分配 {{ allocateResult.success_count }} 台,失败 {{ allocateResult.fail_count }} 台
|
成功分配 {{ allocateResult.success_count }} 台,失败
|
||||||
|
{{ allocateResult.fail_count }} 台
|
||||||
</template>
|
</template>
|
||||||
</ElAlert>
|
</ElAlert>
|
||||||
<div v-if="allocateResult.failed_items && allocateResult.failed_items.length > 0">
|
<div v-if="allocateResult.failed_items && allocateResult.failed_items.length > 0">
|
||||||
@@ -101,7 +107,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="item in allocateResult.failed_items"
|
v-for="item in allocateResult.failed_items"
|
||||||
:key="item.device_id"
|
:key="item.device_id"
|
||||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||||||
>
|
>
|
||||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +135,7 @@
|
|||||||
<ElDialog v-model="recallDialogVisible" title="批量回收设备" width="600px">
|
<ElDialog v-model="recallDialogVisible" title="批量回收设备" width="600px">
|
||||||
<ElForm ref="recallFormRef" :model="recallForm" label-width="120px">
|
<ElForm ref="recallFormRef" :model="recallForm" label-width="120px">
|
||||||
<ElFormItem label="已选设备数">
|
<ElFormItem label="已选设备数">
|
||||||
<span style="color: #e6a23c; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
<span style="font-weight: bold; color: #e6a23c">{{ selectedDevices.length }}</span> 台
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="备注">
|
<ElFormItem label="备注">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -157,7 +163,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="item in recallResult.failed_items"
|
v-for="item in recallResult.failed_items"
|
||||||
:key="item.device_id"
|
:key="item.device_id"
|
||||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||||||
>
|
>
|
||||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||||
</div>
|
</div>
|
||||||
@@ -182,10 +188,19 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 批量设置套餐系列绑定对话框 -->
|
<!-- 批量设置套餐系列绑定对话框 -->
|
||||||
<ElDialog v-model="seriesBindingDialogVisible" title="批量设置设备套餐系列绑定" width="600px">
|
<ElDialog
|
||||||
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
|
v-model="seriesBindingDialogVisible"
|
||||||
|
title="批量设置设备套餐系列绑定"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<ElForm
|
||||||
|
ref="seriesBindingFormRef"
|
||||||
|
:model="seriesBindingForm"
|
||||||
|
:rules="seriesBindingRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
<ElFormItem label="已选设备数">
|
<ElFormItem label="已选设备数">
|
||||||
<span style="color: #409eff; font-weight: bold">{{ selectedDevices.length }}</span> 台
|
<span style="font-weight: bold; color: #409eff">{{ selectedDevices.length }}</span> 台
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
|
<ElFormItem label="套餐系列分配" prop="series_allocation_id">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
@@ -214,15 +229,18 @@
|
|||||||
style="margin-bottom: 10px"
|
style="margin-bottom: 10px"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
成功设置 {{ seriesBindingResult.success_count }} 台,失败 {{ seriesBindingResult.fail_count }} 台
|
成功设置 {{ seriesBindingResult.success_count }} 台,失败
|
||||||
|
{{ seriesBindingResult.fail_count }} 台
|
||||||
</template>
|
</template>
|
||||||
</ElAlert>
|
</ElAlert>
|
||||||
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0">
|
<div
|
||||||
|
v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0"
|
||||||
|
>
|
||||||
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
<div style="margin-bottom: 10px; font-weight: bold">失败详情:</div>
|
||||||
<div
|
<div
|
||||||
v-for="item in seriesBindingResult.failed_items"
|
v-for="item in seriesBindingResult.failed_items"
|
||||||
:key="item.device_id"
|
:key="item.device_id"
|
||||||
style="margin-bottom: 8px; color: #f56c6c; font-size: 12px"
|
style="margin-bottom: 8px; font-size: 12px; color: #f56c6c"
|
||||||
>
|
>
|
||||||
设备号: {{ item.device_no }} - {{ item.reason }}
|
设备号: {{ item.device_no }} - {{ item.reason }}
|
||||||
</div>
|
</div>
|
||||||
@@ -245,6 +263,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 设备详情弹窗 -->
|
||||||
|
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="900px">
|
||||||
|
<div v-if="deviceDetailLoading" style="text-align: center; padding: 40px 0">
|
||||||
|
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||||
|
</div>
|
||||||
|
<ElDescriptions v-else-if="currentDeviceDetail" :column="3" border>
|
||||||
|
<ElDescriptionsItem label="设备ID">{{ currentDeviceDetail.id }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备号" :span="2">{{
|
||||||
|
currentDeviceDetail.device_no
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="设备名称">{{
|
||||||
|
currentDeviceDetail.device_name || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备型号">{{
|
||||||
|
currentDeviceDetail.device_model || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备类型">{{
|
||||||
|
currentDeviceDetail.device_type || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="制造商">{{
|
||||||
|
currentDeviceDetail.manufacturer || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="最大插槽数">{{
|
||||||
|
currentDeviceDetail.max_sim_slots
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="已绑定卡数量">{{
|
||||||
|
currentDeviceDetail.bound_card_count
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="状态">
|
||||||
|
<ElTag :type="getDeviceStatusTagType(currentDeviceDetail.status)">
|
||||||
|
{{ currentDeviceDetail.status_name }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="店铺名称">{{
|
||||||
|
currentDeviceDetail.shop_name || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="批次号">{{
|
||||||
|
currentDeviceDetail.batch_no || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="激活时间">{{
|
||||||
|
currentDeviceDetail.activated_at || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建时间">{{
|
||||||
|
currentDeviceDetail.created_at || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="更新时间">{{
|
||||||
|
currentDeviceDetail.updated_at || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</ElDialog>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -255,7 +328,8 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { DeviceService, ShopService } from '@/api/modules'
|
import { DeviceService, ShopService } from '@/api/modules'
|
||||||
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
||||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElIcon } from 'element-plus'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type {
|
import type {
|
||||||
Device,
|
Device,
|
||||||
@@ -301,6 +375,11 @@
|
|||||||
})
|
})
|
||||||
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
|
||||||
|
|
||||||
|
// 设备详情弹窗相关
|
||||||
|
const deviceDetailDialogVisible = ref(false)
|
||||||
|
const deviceDetailLoading = ref(false)
|
||||||
|
const currentDeviceDetail = ref<any>(null)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
device_no: '',
|
device_no: '',
|
||||||
@@ -424,6 +503,40 @@
|
|||||||
remark: ''
|
remark: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 查看设备详情(通过弹窗)
|
||||||
|
const goToDeviceSearchDetail = async (deviceNo: string) => {
|
||||||
|
deviceDetailDialogVisible.value = true
|
||||||
|
deviceDetailLoading.value = true
|
||||||
|
currentDeviceDetail.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.getDeviceByImei(deviceNo)
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
currentDeviceDetail.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '查询失败')
|
||||||
|
deviceDetailDialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('查询设备详情失败:', error)
|
||||||
|
ElMessage.error(error?.message || '查询失败,请检查设备号是否正确')
|
||||||
|
deviceDetailDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
deviceDetailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取设备状态标签类型
|
||||||
|
const getDeviceStatusTagType = (status: number) => {
|
||||||
|
const typeMap: Record<number, any> = {
|
||||||
|
1: 'info', // 在库
|
||||||
|
2: 'warning', // 已分销
|
||||||
|
3: 'success', // 已激活
|
||||||
|
4: 'danger' // 已停用
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
@@ -434,7 +547,17 @@
|
|||||||
{
|
{
|
||||||
prop: 'device_no',
|
prop: 'device_no',
|
||||||
label: '设备号',
|
label: '设备号',
|
||||||
minWidth: 150
|
minWidth: 150,
|
||||||
|
formatter: (row: Device) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
|
||||||
|
onClick: () => goToDeviceSearchDetail(row.device_no)
|
||||||
|
},
|
||||||
|
row.device_no
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'device_name',
|
prop: 'device_name',
|
||||||
|
|||||||
@@ -18,9 +18,7 @@
|
|||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<ElButton type="primary" :loading="loading" @click="handleSearch">
|
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
|
||||||
查询
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
</template>
|
||||||
</ElInput>
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -38,25 +36,41 @@
|
|||||||
|
|
||||||
<ElDescriptions :column="3" border>
|
<ElDescriptions :column="3" border>
|
||||||
<ElDescriptionsItem label="设备ID">{{ deviceDetail.id }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="设备ID">{{ deviceDetail.id }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="设备号" :span="2">{{ deviceDetail.device_no }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="设备号" :span="2">{{
|
||||||
|
deviceDetail.device_no
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="设备名称">{{ deviceDetail.device_name || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="设备名称">{{
|
||||||
<ElDescriptionsItem label="设备型号">{{ deviceDetail.device_model || '--' }}</ElDescriptionsItem>
|
deviceDetail.device_name || '--'
|
||||||
<ElDescriptionsItem label="设备类型">{{ deviceDetail.device_type || '--' }}</ElDescriptionsItem>
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备型号">{{
|
||||||
|
deviceDetail.device_model || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="设备类型">{{
|
||||||
|
deviceDetail.device_type || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="制造商">{{ deviceDetail.manufacturer || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="制造商">{{
|
||||||
|
deviceDetail.manufacturer || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="最大插槽数">{{ deviceDetail.max_sim_slots }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="最大插槽数">{{ deviceDetail.max_sim_slots }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="已绑定卡数量">{{ deviceDetail.bound_card_count }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="已绑定卡数量">{{
|
||||||
|
deviceDetail.bound_card_count
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="状态">
|
<ElDescriptionsItem label="状态">
|
||||||
<ElTag :type="getStatusTagType(deviceDetail.status)">
|
<ElTag :type="getStatusTagType(deviceDetail.status)">
|
||||||
{{ deviceDetail.status_name }}
|
{{ deviceDetail.status_name }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="店铺名称">{{ deviceDetail.shop_name || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="店铺名称">{{
|
||||||
|
deviceDetail.shop_name || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="批次号">{{ deviceDetail.batch_no }}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="激活时间">{{ deviceDetail.activated_at || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="激活时间">{{
|
||||||
|
deviceDetail.activated_at || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="创建时间">{{ deviceDetail.created_at }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="更新时间">{{ deviceDetail.updated_at }}</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
v-model:filter="searchForm"
|
v-model:filter="searchForm"
|
||||||
:items="searchFormItems"
|
:items="searchFormItems"
|
||||||
|
:show-expand="false"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
></ArtSearchBar>
|
></ArtSearchBar>
|
||||||
@@ -15,7 +16,13 @@
|
|||||||
:columnList="columnOptions"
|
:columnList="columnOptions"
|
||||||
v-model:columns="columnChecks"
|
v-model:columns="columnChecks"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
/>
|
>
|
||||||
|
<template #left>
|
||||||
|
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
|
||||||
|
批量导入设备
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ArtTableHeader>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<ArtTable
|
<ArtTable
|
||||||
@@ -36,25 +43,149 @@
|
|||||||
</ArtTable>
|
</ArtTable>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入对话框 -->
|
||||||
|
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="700px" align-center>
|
||||||
|
<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.
|
||||||
|
必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
>5.
|
||||||
|
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElAlert>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px">
|
||||||
|
<ElButton type="primary" :icon="Download" @click="downloadTemplate">
|
||||||
|
下载导入模板
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="handleCancelImport">取消</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:loading="uploading"
|
||||||
|
:disabled="!fileList.length"
|
||||||
|
@click="submitUpload"
|
||||||
|
>
|
||||||
|
开始导入
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 任务详情对话框 -->
|
||||||
|
<ElDialog v-model="detailDialogVisible" title="设备导入任务详情" width="900px" align-center>
|
||||||
|
<ElDescriptions :column="2" border>
|
||||||
|
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||||
|
currentDetail.task_no
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="状态">
|
||||||
|
<ElTag :type="getStatusType(currentDetail.status)">{{ currentDetail.status_text }}</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="批次号">{{ currentDetail.batch_no }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||||
|
currentDetail.file_name
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总数">{{ currentDetail.total_count }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="成功数">
|
||||||
|
<span style="color: var(--el-color-success)">{{ currentDetail.success_count }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="跳过数">{{ currentDetail.skip_count }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="失败数">
|
||||||
|
<span style="color: var(--el-color-danger)">{{ currentDetail.fail_count }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="警告数">
|
||||||
|
<span style="color: var(--el-color-warning)">{{ currentDetail.warning_count }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="开始时间">{{ currentDetail.started_at }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="完成时间">{{ currentDetail.completed_at }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建时间">{{ currentDetail.created_at }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||||
|
currentDetail.error_message
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<ElDivider content-position="left">失败明细</ElDivider>
|
||||||
|
<div
|
||||||
|
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
|
||||||
|
style="max-height: 300px; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<ElTable :data="currentDetail.failed_items" border size="small">
|
||||||
|
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" />
|
||||||
|
<ElTableColumn label="设备编号" prop="device_no" width="150" />
|
||||||
|
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||||
|
<ElTableColumn label="失败原因" prop="reason" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.reason || row.error || '未知错误' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
<ElEmpty v-else description="无失败记录" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="currentDetail.fail_count > 0"
|
||||||
|
type="primary"
|
||||||
|
:icon="Download"
|
||||||
|
@click="downloadFailData"
|
||||||
|
>
|
||||||
|
下载失败数据
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { DeviceService } from '@/api/modules'
|
import { DeviceService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag } from 'element-plus'
|
import { ElMessage, ElTag } from 'element-plus'
|
||||||
|
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||||
|
import type { UploadInstance } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import { StorageService } from '@/api/modules/storage'
|
||||||
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
|
import type { DeviceImportTask, DeviceImportTaskStatus } from '@/types/api/device'
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceTask' })
|
defineOptions({ name: 'DeviceTask' })
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
const uploadRef = ref<UploadInstance>()
|
||||||
|
const fileList = ref<File[]>([])
|
||||||
|
const uploading = ref(false)
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
@@ -116,19 +247,21 @@
|
|||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: '任务编号', prop: 'task_no' },
|
{ label: '任务编号', prop: 'task_no' },
|
||||||
{ label: '批次号', prop: 'batch_no' },
|
|
||||||
{ label: '文件名', prop: 'file_name' },
|
|
||||||
{ label: '任务状态', prop: 'status' },
|
{ label: '任务状态', prop: 'status' },
|
||||||
|
{ label: '文件名', prop: 'file_name' },
|
||||||
{ label: '总数', prop: 'total_count' },
|
{ label: '总数', prop: 'total_count' },
|
||||||
{ label: '成功数', prop: 'success_count' },
|
{ label: '成功数', prop: 'success_count' },
|
||||||
{ label: '失败数', prop: 'fail_count' },
|
{ label: '失败数', prop: 'fail_count' },
|
||||||
{ label: '跳过数', prop: 'skip_count' },
|
{ label: '跳过数', prop: 'skip_count' },
|
||||||
{ label: '创建时间', prop: 'created_at' },
|
{ label: '开始时间', prop: 'started_at' },
|
||||||
{ label: '完成时间', prop: 'completed_at' },
|
{ label: '完成时间', prop: 'completed_at' },
|
||||||
|
{ label: '错误信息', prop: 'error_message' },
|
||||||
|
{ label: '创建时间', prop: 'created_at' },
|
||||||
{ label: '操作', prop: 'operation' }
|
{ label: '操作', prop: 'operation' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const taskList = ref<DeviceImportTask[]>([])
|
const taskList = ref<DeviceImportTask[]>([])
|
||||||
|
const currentDetail = ref<any>({})
|
||||||
|
|
||||||
// 获取状态标签类型
|
// 获取状态标签类型
|
||||||
const getStatusType = (status: DeviceImportTaskStatus) => {
|
const getStatusType = (status: DeviceImportTaskStatus) => {
|
||||||
@@ -147,14 +280,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
// 查看详情
|
||||||
const viewDetail = (row: DeviceImportTask) => {
|
const viewDetail = async (row: DeviceImportTask) => {
|
||||||
router.push({
|
try {
|
||||||
path: '/asset-management/task-detail',
|
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||||
query: {
|
if (res.code === 0 && res.data) {
|
||||||
id: row.id,
|
currentDetail.value = {
|
||||||
task_type: 'device'
|
...res.data,
|
||||||
|
started_at: res.data.started_at ? formatDateTime(res.data.started_at) : '-',
|
||||||
|
completed_at: res.data.completed_at ? formatDateTime(res.data.completed_at) : '-',
|
||||||
|
created_at: res.data.created_at ? formatDateTime(res.data.created_at) : '-'
|
||||||
|
}
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error)
|
||||||
|
ElMessage.error('获取任务详情失败')
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
@@ -162,17 +303,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'task_no',
|
prop: 'task_no',
|
||||||
label: '任务编号',
|
label: '任务编号',
|
||||||
width: 150
|
width: 180
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'batch_no',
|
|
||||||
label: '批次号',
|
|
||||||
width: 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'file_name',
|
|
||||||
label: '文件名',
|
|
||||||
minWidth: 200
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -182,6 +313,12 @@
|
|||||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'file_name',
|
||||||
|
label: '文件名',
|
||||||
|
minWidth: 250,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'total_count',
|
prop: 'total_count',
|
||||||
label: '总数',
|
label: '总数',
|
||||||
@@ -190,15 +327,17 @@
|
|||||||
{
|
{
|
||||||
prop: 'success_count',
|
prop: 'success_count',
|
||||||
label: '成功数',
|
label: '成功数',
|
||||||
width: 80
|
width: 80,
|
||||||
|
formatter: (row: DeviceImportTask) => {
|
||||||
|
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'fail_count',
|
prop: 'fail_count',
|
||||||
label: '失败数',
|
label: '失败数',
|
||||||
width: 80,
|
width: 80,
|
||||||
formatter: (row: DeviceImportTask) => {
|
formatter: (row: DeviceImportTask) => {
|
||||||
const type = row.fail_count > 0 ? 'danger' : 'success'
|
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||||
return h(ElTag, { type, size: 'small' }, () => row.fail_count)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -207,27 +346,59 @@
|
|||||||
width: 80
|
width: 80
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'created_at',
|
prop: 'started_at',
|
||||||
label: '创建时间',
|
label: '开始时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: (row: DeviceImportTask) => formatDateTime(row.created_at)
|
formatter: (row: DeviceImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'completed_at',
|
prop: 'completed_at',
|
||||||
label: '完成时间',
|
label: '完成时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: (row: DeviceImportTask) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
formatter: (row: DeviceImportTask) =>
|
||||||
|
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'error_message',
|
||||||
|
label: '错误信息',
|
||||||
|
minWidth: 200,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: DeviceImportTask) => row.error_message || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'created_at',
|
||||||
|
label: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: DeviceImportTask) => formatDateTime(row.created_at)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 100,
|
width: 180,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: DeviceImportTask) => {
|
formatter: (row: DeviceImportTask) => {
|
||||||
return h(ArtButtonTable, {
|
const buttons = []
|
||||||
type: 'view',
|
|
||||||
|
// 显示"查看详情"按钮
|
||||||
|
buttons.push(
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '详情',
|
||||||
onClick: () => viewDetail(row)
|
onClick: () => viewDetail(row)
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果有失败数据,显示"失败数据"按钮
|
||||||
|
if (row.fail_count > 0) {
|
||||||
|
buttons.push(
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '失败数据',
|
||||||
|
type: 'danger',
|
||||||
|
onClick: () => downloadFailDataByRow(row)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -262,7 +433,7 @@
|
|||||||
|
|
||||||
const res = await DeviceService.getImportTasks(params)
|
const res = await DeviceService.getImportTasks(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
taskList.value = res.data.list || []
|
taskList.value = res.data.items || []
|
||||||
pagination.total = res.data.total || 0
|
pagination.total = res.data.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -301,10 +472,195 @@
|
|||||||
pagination.page = newCurrentPage
|
pagination.page = newCurrentPage
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载模板
|
||||||
|
const downloadTemplate = () => {
|
||||||
|
const csvContent = [
|
||||||
|
'device_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')
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消导入
|
||||||
|
const handleCancelImport = () => {
|
||||||
|
clearFiles()
|
||||||
|
importDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交上传
|
||||||
|
const submitUpload = async () => {
|
||||||
|
if (!fileList.value.length) {
|
||||||
|
ElMessage.warning('请先选择CSV文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileList.value[0]
|
||||||
|
uploading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
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')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
handleCancelImport()
|
||||||
|
getTableData()
|
||||||
|
|
||||||
|
ElMessage.success({
|
||||||
|
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||||
|
duration: 3000,
|
||||||
|
showClose: true
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('设备导入失败:', error)
|
||||||
|
ElMessage.error(error.message || '设备导入失败')
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从行数据下载失败数据
|
||||||
|
const downloadFailDataByRow = async (row: DeviceImportTask) => {
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
const detail = res.data
|
||||||
|
downloadFailDataFromDetail(detail, row.batch_no)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败数据失败:', error)
|
||||||
|
ElMessage.error('下载失败数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载失败数据(从详情对话框)
|
||||||
|
const downloadFailData = () => {
|
||||||
|
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.batch_no)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载失败数据的通用方法
|
||||||
|
const downloadFailDataFromDetail = (detail: any, batchNo: string) => {
|
||||||
|
const failReasons =
|
||||||
|
detail.failed_items?.map((item: any, index: number) => ({
|
||||||
|
row: index + 1,
|
||||||
|
deviceCode: item.device_no || '-',
|
||||||
|
iccid: item.iccid || '-',
|
||||||
|
message: item.reason || item.error || '未知错误'
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
if (failReasons.length === 0) {
|
||||||
|
ElMessage.warning('没有失败数据可下载')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = ['行号', '设备编号', 'ICCID', '失败原因']
|
||||||
|
const csvRows = [
|
||||||
|
headers.join(','),
|
||||||
|
...failReasons.map((item: any) =>
|
||||||
|
[item.row, item.deviceCode, item.iccid, `"${item.message}"`].join(',')
|
||||||
|
)
|
||||||
|
]
|
||||||
|
const csvContent = csvRows.join('\n')
|
||||||
|
|
||||||
|
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', `导入失败数据_${batchNo}.csv`)
|
||||||
|
link.style.visibility = 'hidden'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage.success('失败数据下载成功')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.device-task-page {
|
.device-task-page {
|
||||||
// Device task page styles
|
: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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
:placeholder="$t('enterpriseDevices.form.deviceNosPlaceholder')"
|
:placeholder="$t('enterpriseDevices.form.deviceNosPlaceholder')"
|
||||||
@input="handleDeviceNosChange"
|
@input="handleDeviceNosChange"
|
||||||
/>
|
/>
|
||||||
<div style="color: var(--el-color-info); margin-top: 4px; font-size: 12px">
|
<div style="margin-top: 4px; font-size: 12px; color: var(--el-color-info)">
|
||||||
{{
|
{{
|
||||||
$t('enterpriseDevices.form.selectedCount', {
|
$t('enterpriseDevices.form.selectedCount', {
|
||||||
count: allocateForm.device_nos?.length || 0
|
count: allocateForm.device_nos?.length || 0
|
||||||
@@ -110,9 +110,7 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="allocateDialogVisible = false">{{
|
<ElButton @click="allocateDialogVisible = false">{{ $t('common.cancel') }}</ElButton>
|
||||||
$t('common.cancel')
|
|
||||||
}}</ElButton>
|
|
||||||
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
|
<ElButton type="primary" @click="handleAllocate" :loading="allocateLoading">
|
||||||
{{ $t('common.confirm') }}
|
{{ $t('common.confirm') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -635,8 +633,8 @@
|
|||||||
.enterprise-devices-page {
|
.enterprise-devices-page {
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,13 +19,25 @@
|
|||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton type="primary" @click="showImportDialog">导入ICCID</ElButton>
|
<ElButton type="primary" @click="showImportDialog">导入ICCID</ElButton>
|
||||||
<ElButton type="success" :disabled="selectedCards.length === 0" @click="showAllocateDialog">
|
<ElButton
|
||||||
|
type="success"
|
||||||
|
:disabled="selectedCards.length === 0"
|
||||||
|
@click="showAllocateDialog"
|
||||||
|
>
|
||||||
批量分配
|
批量分配
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton type="warning" :disabled="selectedCards.length === 0" @click="showRecallDialog">
|
<ElButton
|
||||||
|
type="warning"
|
||||||
|
:disabled="selectedCards.length === 0"
|
||||||
|
@click="showRecallDialog"
|
||||||
|
>
|
||||||
批量回收
|
批量回收
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton type="info" :disabled="selectedCards.length === 0" @click="showSeriesBindingDialog">
|
<ElButton
|
||||||
|
type="info"
|
||||||
|
:disabled="selectedCards.length === 0"
|
||||||
|
@click="showSeriesBindingDialog"
|
||||||
|
>
|
||||||
批量设置套餐系列
|
批量设置套餐系列
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton type="primary" @click="cardDistribution">网卡分销</ElButton>
|
<ElButton type="primary" @click="cardDistribution">网卡分销</ElButton>
|
||||||
@@ -65,7 +77,11 @@
|
|||||||
>
|
>
|
||||||
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="100px">
|
<ElForm ref="importFormRef" :model="importForm" :rules="importRules" label-width="100px">
|
||||||
<ElFormItem label="运营商" prop="carrier_id">
|
<ElFormItem label="运营商" prop="carrier_id">
|
||||||
<ElSelect v-model="importForm.carrier_id" placeholder="请选择运营商" style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="importForm.carrier_id"
|
||||||
|
placeholder="请选择运营商"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption label="中国移动" :value="1" />
|
<ElOption label="中国移动" :value="1" />
|
||||||
<ElOption label="中国联通" :value="2" />
|
<ElOption label="中国联通" :value="2" />
|
||||||
<ElOption label="中国电信" :value="3" />
|
<ElOption label="中国电信" :value="3" />
|
||||||
@@ -91,7 +107,7 @@
|
|||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
<div>只支持上传CSV文件,且不超过10MB</div>
|
<div>只支持上传CSV文件,且不超过10MB</div>
|
||||||
<div style="color: var(--el-color-info); margin-top: 4px">
|
<div style="margin-top: 4px; color: var(--el-color-info)">
|
||||||
CSV格式:ICCID,MSISDN(两列,逗号分隔,每行一条记录)
|
CSV格式:ICCID,MSISDN(两列,逗号分隔,每行一条记录)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,9 +132,18 @@
|
|||||||
width="600px"
|
width="600px"
|
||||||
@close="handleAllocateDialogClose"
|
@close="handleAllocateDialogClose"
|
||||||
>
|
>
|
||||||
<ElForm ref="allocateFormRef" :model="allocateForm" :rules="allocateRules" label-width="120px">
|
<ElForm
|
||||||
|
ref="allocateFormRef"
|
||||||
|
:model="allocateForm"
|
||||||
|
:rules="allocateRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
<ElFormItem label="目标店铺" prop="to_shop_id">
|
<ElFormItem label="目标店铺" prop="to_shop_id">
|
||||||
<ElSelect v-model="allocateForm.to_shop_id" placeholder="请选择目标店铺" style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="allocateForm.to_shop_id"
|
||||||
|
placeholder="请选择目标店铺"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption label="店铺A" :value="1" />
|
<ElOption label="店铺A" :value="1" />
|
||||||
<ElOption label="店铺B" :value="2" />
|
<ElOption label="店铺B" :value="2" />
|
||||||
<ElOption label="店铺C" :value="3" />
|
<ElOption label="店铺C" :value="3" />
|
||||||
@@ -136,22 +161,40 @@
|
|||||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem v-if="allocateForm.selection_type === 'range'" label="起始ICCID" prop="iccid_start">
|
<ElFormItem
|
||||||
|
v-if="allocateForm.selection_type === 'range'"
|
||||||
|
label="起始ICCID"
|
||||||
|
prop="iccid_start"
|
||||||
|
>
|
||||||
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
|
<ElInput v-model="allocateForm.iccid_start" placeholder="请输入起始ICCID" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="allocateForm.selection_type === 'range'" label="结束ICCID" prop="iccid_end">
|
<ElFormItem
|
||||||
|
v-if="allocateForm.selection_type === 'range'"
|
||||||
|
label="结束ICCID"
|
||||||
|
prop="iccid_end"
|
||||||
|
>
|
||||||
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
|
<ElInput v-model="allocateForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="运营商">
|
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="运营商">
|
||||||
<ElSelect v-model="allocateForm.carrier_id" placeholder="请选择运营商" clearable style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="allocateForm.carrier_id"
|
||||||
|
placeholder="请选择运营商"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption label="中国移动" :value="1" />
|
<ElOption label="中国移动" :value="1" />
|
||||||
<ElOption label="中国联通" :value="2" />
|
<ElOption label="中国联通" :value="2" />
|
||||||
<ElOption label="中国电信" :value="3" />
|
<ElOption label="中国电信" :value="3" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="卡状态">
|
<ElFormItem v-if="allocateForm.selection_type === 'filter'" label="卡状态">
|
||||||
<ElSelect v-model="allocateForm.status" placeholder="请选择状态" clearable style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="allocateForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption label="在库" :value="1" />
|
<ElOption label="在库" :value="1" />
|
||||||
<ElOption label="已分销" :value="2" />
|
<ElOption label="已分销" :value="2" />
|
||||||
<ElOption label="已激活" :value="3" />
|
<ElOption label="已激活" :value="3" />
|
||||||
@@ -163,7 +206,12 @@
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="备注">
|
<ElFormItem label="备注">
|
||||||
<ElInput v-model="allocateForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
<ElInput
|
||||||
|
v-model="allocateForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -185,7 +233,11 @@
|
|||||||
>
|
>
|
||||||
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
|
<ElForm ref="recallFormRef" :model="recallForm" :rules="recallRules" label-width="120px">
|
||||||
<ElFormItem label="来源店铺" prop="from_shop_id">
|
<ElFormItem label="来源店铺" prop="from_shop_id">
|
||||||
<ElSelect v-model="recallForm.from_shop_id" placeholder="请选择来源店铺" style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="recallForm.from_shop_id"
|
||||||
|
placeholder="请选择来源店铺"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption label="店铺A" :value="1" />
|
<ElOption label="店铺A" :value="1" />
|
||||||
<ElOption label="店铺B" :value="2" />
|
<ElOption label="店铺B" :value="2" />
|
||||||
<ElOption label="店铺C" :value="3" />
|
<ElOption label="店铺C" :value="3" />
|
||||||
@@ -203,15 +255,28 @@
|
|||||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem v-if="recallForm.selection_type === 'range'" label="起始ICCID" prop="iccid_start">
|
<ElFormItem
|
||||||
|
v-if="recallForm.selection_type === 'range'"
|
||||||
|
label="起始ICCID"
|
||||||
|
prop="iccid_start"
|
||||||
|
>
|
||||||
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
|
<ElInput v-model="recallForm.iccid_start" placeholder="请输入起始ICCID" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem v-if="recallForm.selection_type === 'range'" label="结束ICCID" prop="iccid_end">
|
<ElFormItem
|
||||||
|
v-if="recallForm.selection_type === 'range'"
|
||||||
|
label="结束ICCID"
|
||||||
|
prop="iccid_end"
|
||||||
|
>
|
||||||
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
|
<ElInput v-model="recallForm.iccid_end" placeholder="请输入结束ICCID" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="运营商">
|
<ElFormItem v-if="recallForm.selection_type === 'filter'" label="运营商">
|
||||||
<ElSelect v-model="recallForm.carrier_id" placeholder="请选择运营商" clearable style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="recallForm.carrier_id"
|
||||||
|
placeholder="请选择运营商"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<ElOption label="中国移动" :value="1" />
|
<ElOption label="中国移动" :value="1" />
|
||||||
<ElOption label="中国联通" :value="2" />
|
<ElOption label="中国联通" :value="2" />
|
||||||
<ElOption label="中国电信" :value="3" />
|
<ElOption label="中国电信" :value="3" />
|
||||||
@@ -222,7 +287,12 @@
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem label="备注">
|
<ElFormItem label="备注">
|
||||||
<ElInput v-model="recallForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
|
<ElInput
|
||||||
|
v-model="recallForm.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -236,14 +306,14 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 分配结果对话框 -->
|
<!-- 分配结果对话框 -->
|
||||||
<ElDialog
|
<ElDialog v-model="resultDialogVisible" :title="resultTitle" width="700px">
|
||||||
v-model="resultDialogVisible"
|
|
||||||
:title="resultTitle"
|
|
||||||
width="700px"
|
|
||||||
>
|
|
||||||
<ElDescriptions :column="2" border>
|
<ElDescriptions :column="2" border>
|
||||||
<ElDescriptionsItem label="操作单号">{{ allocationResult.allocation_no }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="操作单号">{{
|
||||||
<ElDescriptionsItem label="待处理总数">{{ allocationResult.total_count }}</ElDescriptionsItem>
|
allocationResult.allocation_no
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="待处理总数">{{
|
||||||
|
allocationResult.total_count
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="成功数">
|
<ElDescriptionsItem label="成功数">
|
||||||
<ElTag type="success">{{ allocationResult.success_count }}</ElTag>
|
<ElTag type="success">{{ allocationResult.success_count }}</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
@@ -252,7 +322,10 @@
|
|||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<div v-if="allocationResult.failed_items && allocationResult.failed_items.length > 0" style="margin-top: 20px">
|
<div
|
||||||
|
v-if="allocationResult.failed_items && allocationResult.failed_items.length > 0"
|
||||||
|
style="margin-top: 20px"
|
||||||
|
>
|
||||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||||
<ElTable :data="allocationResult.failed_items" border max-height="300">
|
<ElTable :data="allocationResult.failed_items" border max-height="300">
|
||||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
||||||
@@ -274,7 +347,12 @@
|
|||||||
width="600px"
|
width="600px"
|
||||||
@close="handleSeriesBindingDialogClose"
|
@close="handleSeriesBindingDialogClose"
|
||||||
>
|
>
|
||||||
<ElForm ref="seriesBindingFormRef" :model="seriesBindingForm" :rules="seriesBindingRules" label-width="120px">
|
<ElForm
|
||||||
|
ref="seriesBindingFormRef"
|
||||||
|
:model="seriesBindingForm"
|
||||||
|
:rules="seriesBindingRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
<ElFormItem label="已选择卡数">
|
<ElFormItem label="已选择卡数">
|
||||||
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
<div>已选择 {{ selectedCards.length }} 张卡</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -307,11 +385,7 @@
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 套餐系列绑定结果对话框 -->
|
<!-- 套餐系列绑定结果对话框 -->
|
||||||
<ElDialog
|
<ElDialog v-model="seriesBindingResultDialogVisible" title="设置结果" width="700px">
|
||||||
v-model="seriesBindingResultDialogVisible"
|
|
||||||
title="设置结果"
|
|
||||||
width="700px"
|
|
||||||
>
|
|
||||||
<ElDescriptions :column="2" border>
|
<ElDescriptions :column="2" border>
|
||||||
<ElDescriptionsItem label="成功数">
|
<ElDescriptionsItem label="成功数">
|
||||||
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
|
<ElTag type="success">{{ seriesBindingResult.success_count }}</ElTag>
|
||||||
@@ -321,7 +395,10 @@
|
|||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<div v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0" style="margin-top: 20px">
|
<div
|
||||||
|
v-if="seriesBindingResult.failed_items && seriesBindingResult.failed_items.length > 0"
|
||||||
|
style="margin-top: 20px"
|
||||||
|
>
|
||||||
<ElDivider content-position="left">失败项详情</ElDivider>
|
<ElDivider content-position="left">失败项详情</ElDivider>
|
||||||
<ElTable :data="seriesBindingResult.failed_items" border max-height="300">
|
<ElTable :data="seriesBindingResult.failed_items" border max-height="300">
|
||||||
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
<ElTableColumn prop="iccid" label="ICCID" width="180" />
|
||||||
@@ -331,11 +408,99 @@
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false">确定</ElButton>
|
<ElButton type="primary" @click="seriesBindingResultDialogVisible = false"
|
||||||
|
>确定</ElButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 卡详情对话框 -->
|
||||||
|
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
|
||||||
|
<div v-if="cardDetailLoading" style="text-align: center; padding: 40px">
|
||||||
|
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||||
|
<div style="margin-top: 16px">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElDescriptions v-else-if="currentCardDetail" :column="3" border>
|
||||||
|
<ElDescriptionsItem label="卡ID">{{ currentCardDetail.id }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="ICCID" :span="2">{{
|
||||||
|
currentCardDetail.iccid
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="卡接入号">{{
|
||||||
|
currentCardDetail.msisdn || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="运营商">{{
|
||||||
|
currentCardDetail.carrier_name || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="运营商类型">{{
|
||||||
|
getCarrierTypeText(currentCardDetail.carrier_type)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="卡类型">{{
|
||||||
|
currentCardDetail.card_type || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="卡业务类型">{{
|
||||||
|
getCardCategoryText(currentCardDetail.card_category)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="成本价">{{
|
||||||
|
formatCardPrice(currentCardDetail.cost_price)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="分销价">{{
|
||||||
|
formatCardPrice(currentCardDetail.distribute_price)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="状态">
|
||||||
|
<ElTag :type="getCardDetailStatusTagType(currentCardDetail.status)">
|
||||||
|
{{ getCardDetailStatusText(currentCardDetail.status) }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="激活状态">
|
||||||
|
<ElTag :type="currentCardDetail.activation_status === 1 ? 'success' : 'info'">
|
||||||
|
{{ currentCardDetail.activation_status === 1 ? '已激活' : '未激活' }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="实名状态">
|
||||||
|
<ElTag :type="currentCardDetail.real_name_status === 1 ? 'success' : 'warning'">
|
||||||
|
{{ currentCardDetail.real_name_status === 1 ? '已实名' : '未实名' }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="网络状态">
|
||||||
|
<ElTag :type="currentCardDetail.network_status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ currentCardDetail.network_status === 1 ? '开机' : '停机' }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="累计流量使用"
|
||||||
|
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
||||||
|
>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="首次佣金">
|
||||||
|
<ElTag :type="currentCardDetail.first_commission_paid ? 'success' : 'info'">
|
||||||
|
{{ currentCardDetail.first_commission_paid ? '已支付' : '未支付' }}
|
||||||
|
</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="累计充值">{{
|
||||||
|
formatCardPrice(currentCardDetail.accumulated_recharge)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建时间">{{
|
||||||
|
formatDateTime(currentCardDetail.created_at)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
|
<ElDescriptionsItem label="更新时间" :span="2">{{
|
||||||
|
formatDateTime(currentCardDetail.updated_at)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<ElEmpty v-else description="未找到卡片信息" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton type="primary" @click="cardDetailDialogVisible = false">关闭</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -343,9 +508,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { CardService, StorageService } from '@/api/modules'
|
import { CardService, StorageService } from '@/api/modules'
|
||||||
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
import { ShopSeriesAllocationService } from '@/api/modules/shopSeriesAllocation'
|
||||||
import { ElMessage, ElTag, ElUpload } from 'element-plus'
|
import { ElMessage, ElTag, ElUpload, ElIcon } from 'element-plus'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
|
import type { FormInstance, FormRules, UploadProps, UploadUserFile } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
@@ -362,6 +529,7 @@
|
|||||||
|
|
||||||
defineOptions({ name: 'StandaloneCardList' })
|
defineOptions({ name: 'StandaloneCardList' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const importDialogVisible = ref(false)
|
const importDialogVisible = ref(false)
|
||||||
const importLoading = ref(false)
|
const importLoading = ref(false)
|
||||||
@@ -405,13 +573,17 @@
|
|||||||
failed_items: null
|
failed_items: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 卡详情弹窗相关
|
||||||
|
const cardDetailDialogVisible = ref(false)
|
||||||
|
const cardDetailLoading = ref(false)
|
||||||
|
const currentCardDetail = ref<any>(null)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
status: undefined,
|
status: undefined,
|
||||||
carrier_id: undefined,
|
carrier_id: undefined,
|
||||||
iccid: '',
|
iccid: '',
|
||||||
msisdn: '',
|
msisdn: '',
|
||||||
batch_no: '',
|
|
||||||
is_distributed: undefined
|
is_distributed: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,15 +748,6 @@
|
|||||||
placeholder: '请输入卡接入号'
|
placeholder: '请输入卡接入号'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: '批次号',
|
|
||||||
prop: 'batch_no',
|
|
||||||
type: 'input',
|
|
||||||
config: {
|
|
||||||
clearable: true,
|
|
||||||
placeholder: '请输入批次号'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: '是否已分销',
|
label: '是否已分销',
|
||||||
prop: 'is_distributed',
|
prop: 'is_distributed',
|
||||||
@@ -653,12 +816,88 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开卡详情弹窗
|
||||||
|
const goToCardDetail = async (iccid: string) => {
|
||||||
|
cardDetailDialogVisible.value = true
|
||||||
|
cardDetailLoading.value = true
|
||||||
|
currentCardDetail.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await CardService.getIotCardDetailByIccid(iccid)
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
currentCardDetail.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '查询失败')
|
||||||
|
cardDetailDialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('查询卡片详情失败:', error)
|
||||||
|
ElMessage.error(error?.message || '查询失败,请检查ICCID是否正确')
|
||||||
|
cardDetailDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
cardDetailLoading.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 getCardDetailStatusText = (status: number) => {
|
||||||
|
const statusMap: Record<number, string> = {
|
||||||
|
1: '在库',
|
||||||
|
2: '已分销',
|
||||||
|
3: '已激活',
|
||||||
|
4: '已停用'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCardDetailStatusTagType = (status: number) => {
|
||||||
|
const typeMap: Record<number, any> = {
|
||||||
|
1: 'info',
|
||||||
|
2: 'warning',
|
||||||
|
3: 'success',
|
||||||
|
4: 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatCardPrice = (price: number) => {
|
||||||
|
return `¥${(price / 100).toFixed(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
prop: 'iccid',
|
prop: 'iccid',
|
||||||
label: 'ICCID',
|
label: 'ICCID',
|
||||||
minWidth: 190
|
minWidth: 200,
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: { color: 'var(--el-color-primary)', cursor: 'pointer' },
|
||||||
|
onClick: () => goToCardDetail(row.iccid)
|
||||||
|
},
|
||||||
|
row.iccid
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'msisdn',
|
prop: 'msisdn',
|
||||||
@@ -754,7 +993,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'created_at',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
|
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -897,7 +1136,9 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (importRes.code === 0) {
|
if (importRes.code === 0) {
|
||||||
ElMessage.success(importRes.data.message || '导入任务已创建,请到任务管理页面查看导入进度')
|
ElMessage.success(
|
||||||
|
importRes.data.message || '导入任务已创建,请到任务管理页面查看导入进度'
|
||||||
|
)
|
||||||
importDialogVisible.value = false
|
importDialogVisible.value = false
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1408,9 @@
|
|||||||
} else if (res.data.success_count === 0) {
|
} else if (res.data.success_count === 0) {
|
||||||
ElMessage.error('套餐系列绑定设置失败')
|
ElMessage.error('套餐系列绑定设置失败')
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning(`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count} 项`)
|
ElMessage.warning(
|
||||||
|
`部分设置成功:成功 ${res.data.success_count} 项,失败 ${res.data.fail_count} 项`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -18,9 +18,7 @@
|
|||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<ElButton type="primary" :loading="loading" @click="handleSearch">
|
<ElButton type="primary" :loading="loading" @click="handleSearch"> 查询 </ElButton>
|
||||||
查询
|
|
||||||
</ElButton>
|
|
||||||
</template>
|
</template>
|
||||||
</ElInput>
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -44,9 +42,13 @@
|
|||||||
<ElDescriptionsItem label="卡接入号">{{ cardDetail.msisdn || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="卡接入号">{{ cardDetail.msisdn || '--' }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="运营商">{{ cardDetail.carrier_name }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="运营商">{{ cardDetail.carrier_name }}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="运营商类型">{{ getCarrierTypeText(cardDetail.carrier_type) }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="运营商类型">{{
|
||||||
|
getCarrierTypeText(cardDetail.carrier_type)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="卡类型">{{ cardDetail.card_type }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="卡类型">{{ cardDetail.card_type }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="卡业务类型">{{ getCardCategoryText(cardDetail.card_category) }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="卡业务类型">{{
|
||||||
|
getCardCategoryText(cardDetail.card_category)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="状态">
|
<ElDescriptionsItem label="状态">
|
||||||
<ElTag :type="getStatusTagType(cardDetail.status)">
|
<ElTag :type="getStatusTagType(cardDetail.status)">
|
||||||
@@ -73,11 +75,19 @@
|
|||||||
<ElDescriptionsItem label="供应商">{{ cardDetail.supplier || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="供应商">{{ cardDetail.supplier || '--' }}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="店铺名称">{{ cardDetail.shop_name || '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="店铺名称">{{ cardDetail.shop_name || '--' }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="成本价">{{ formatPrice(cardDetail.cost_price) }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="成本价">{{
|
||||||
<ElDescriptionsItem label="分销价">{{ formatPrice(cardDetail.distribute_price) }}</ElDescriptionsItem>
|
formatPrice(cardDetail.cost_price)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="分销价">{{
|
||||||
|
formatPrice(cardDetail.distribute_price)
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="累计流量使用">{{ cardDetail.data_usage_mb }} MB</ElDescriptionsItem>
|
<ElDescriptionsItem label="累计流量使用"
|
||||||
<ElDescriptionsItem label="激活时间">{{ cardDetail.activated_at || '--' }}</ElDescriptionsItem>
|
>{{ cardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
||||||
|
>
|
||||||
|
<ElDescriptionsItem label="激活时间">{{
|
||||||
|
cardDetail.activated_at || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="创建时间">{{ cardDetail.created_at }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="创建时间">{{ cardDetail.created_at }}</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
v-model:filter="searchForm"
|
v-model:filter="searchForm"
|
||||||
:items="searchFormItems"
|
:items="searchFormItems"
|
||||||
|
:show-expand="false"
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
></ArtSearchBar>
|
></ArtSearchBar>
|
||||||
@@ -15,7 +16,13 @@
|
|||||||
:columnList="columnOptions"
|
:columnList="columnOptions"
|
||||||
v-model:columns="columnChecks"
|
v-model:columns="columnChecks"
|
||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
/>
|
>
|
||||||
|
<template #left>
|
||||||
|
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
|
||||||
|
批量导入IoT卡
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ArtTableHeader>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<ArtTable
|
<ArtTable
|
||||||
@@ -36,25 +43,152 @@
|
|||||||
</ArtTable>
|
</ArtTable>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入对话框 -->
|
||||||
|
<ElDialog v-model="importDialogVisible" title="批量导入IoT卡" width="700px" align-center>
|
||||||
|
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||||
|
<template #title>
|
||||||
|
<div style="line-height: 1.8">
|
||||||
|
<p><strong>导入说明:</strong></p>
|
||||||
|
<p>1. 请先下载 CSV 模板文件,按照模板格式填写IoT卡信息</p>
|
||||||
|
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||||
|
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||||
|
<p>4. 必填字段:iccid(ICCID)、msisdn(MSISDN/手机号)</p>
|
||||||
|
<p>5. 必须选择运营商</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElAlert>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px">
|
||||||
|
<ElButton type="primary" :icon="Download" @click="downloadTemplate">
|
||||||
|
下载导入模板
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem label="运营商" required style="margin-bottom: 20px">
|
||||||
|
<ElSelect v-model="selectedCarrierId" placeholder="请选择运营商" style="width: 100%">
|
||||||
|
<ElOption label="中国移动" :value="1" />
|
||||||
|
<ElOption label="中国联通" :value="2" />
|
||||||
|
<ElOption label="中国电信" :value="3" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="handleCancelImport">取消</ElButton>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
:loading="uploading"
|
||||||
|
:disabled="!fileList.length || !selectedCarrierId"
|
||||||
|
@click="submitUpload"
|
||||||
|
>
|
||||||
|
开始导入
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 任务详情对话框 -->
|
||||||
|
<ElDialog v-model="detailDialogVisible" title="IoT卡导入任务详情" width="900px" align-center>
|
||||||
|
<ElDescriptions :column="2" border>
|
||||||
|
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||||
|
currentDetail.task_no
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="状态">
|
||||||
|
<ElTag :type="getStatusType(currentDetail.status)">{{ currentDetail.status_text }}</ElTag>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="运营商">{{ currentDetail.carrier_name }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||||
|
currentDetail.file_name
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总数">{{ currentDetail.total_count }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="成功数">
|
||||||
|
<span style="color: var(--el-color-success)">{{ currentDetail.success_count }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="跳过数">{{ currentDetail.skip_count }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="失败数">
|
||||||
|
<span style="color: var(--el-color-danger)">{{ currentDetail.fail_count }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="警告数">
|
||||||
|
<span style="color: var(--el-color-warning)">{{ currentDetail.warning_count }}</span>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="开始时间">{{ currentDetail.started_at }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="完成时间">{{ currentDetail.completed_at }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建时间">{{ currentDetail.created_at }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||||
|
currentDetail.error_message
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<ElDivider content-position="left">失败明细</ElDivider>
|
||||||
|
<div
|
||||||
|
v-if="currentDetail.failed_items && currentDetail.failed_items.length"
|
||||||
|
style="max-height: 300px; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<ElTable :data="currentDetail.failed_items" border size="small">
|
||||||
|
<ElTableColumn label="行号" type="index" width="80" :index="(index) => index + 1" />
|
||||||
|
<ElTableColumn label="ICCID" prop="iccid" width="200" />
|
||||||
|
<ElTableColumn label="MSISDN" prop="msisdn" width="150" />
|
||||||
|
<ElTableColumn label="失败原因" prop="reason" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.reason || row.error || '未知错误' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
<ElEmpty v-else description="无失败记录" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="currentDetail.fail_count > 0"
|
||||||
|
type="primary"
|
||||||
|
:icon="Download"
|
||||||
|
@click="downloadFailData"
|
||||||
|
>
|
||||||
|
下载失败数据
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { CardService } from '@/api/modules'
|
import { CardService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag } from 'element-plus'
|
import { ElMessage, ElTag, ElFormItem, ElSelect, ElOption } from 'element-plus'
|
||||||
|
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||||
|
import type { UploadInstance } from 'element-plus'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
|
import { StorageService } from '@/api/modules/storage'
|
||||||
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
||||||
|
|
||||||
defineOptions({ name: 'IotCardTask' })
|
defineOptions({ name: 'IotCardTask' })
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableRef = ref()
|
const tableRef = ref()
|
||||||
|
const uploadRef = ref<UploadInstance>()
|
||||||
|
const fileList = ref<File[]>([])
|
||||||
|
const uploading = ref(false)
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const selectedCarrierId = ref<number>()
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
@@ -145,6 +279,7 @@
|
|||||||
]
|
]
|
||||||
|
|
||||||
const taskList = ref<IotCardImportTask[]>([])
|
const taskList = ref<IotCardImportTask[]>([])
|
||||||
|
const currentDetail = ref<any>({})
|
||||||
|
|
||||||
// 获取状态标签类型
|
// 获取状态标签类型
|
||||||
const getStatusType = (status: IotCardImportTaskStatus) => {
|
const getStatusType = (status: IotCardImportTaskStatus) => {
|
||||||
@@ -163,14 +298,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
// 查看详情
|
||||||
const viewDetail = (row: IotCardImportTask) => {
|
const viewDetail = async (row: IotCardImportTask) => {
|
||||||
router.push({
|
try {
|
||||||
path: '/asset-management/task-detail',
|
const res = await CardService.getIotCardImportTaskDetail(row.id)
|
||||||
query: {
|
if (res.code === 0 && res.data) {
|
||||||
id: row.id,
|
currentDetail.value = {
|
||||||
task_type: 'card'
|
...res.data,
|
||||||
|
started_at: res.data.started_at ? formatDateTime(res.data.started_at) : '-',
|
||||||
|
completed_at: res.data.completed_at ? formatDateTime(res.data.completed_at) : '-',
|
||||||
|
created_at: res.data.created_at ? formatDateTime(res.data.created_at) : '-',
|
||||||
|
carrier_name: res.data.carrier_name || '-',
|
||||||
|
error_message: res.data.error_message || '-'
|
||||||
|
}
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error)
|
||||||
|
ElMessage.error('获取任务详情失败')
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
@@ -196,7 +341,8 @@
|
|||||||
{
|
{
|
||||||
prop: 'file_name',
|
prop: 'file_name',
|
||||||
label: '文件名',
|
label: '文件名',
|
||||||
minWidth: 250
|
minWidth: 250,
|
||||||
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'total_count',
|
prop: 'total_count',
|
||||||
@@ -206,15 +352,17 @@
|
|||||||
{
|
{
|
||||||
prop: 'success_count',
|
prop: 'success_count',
|
||||||
label: '成功数',
|
label: '成功数',
|
||||||
width: 80
|
width: 80,
|
||||||
|
formatter: (row: IotCardImportTask) => {
|
||||||
|
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'fail_count',
|
prop: 'fail_count',
|
||||||
label: '失败数',
|
label: '失败数',
|
||||||
width: 80,
|
width: 80,
|
||||||
formatter: (row: IotCardImportTask) => {
|
formatter: (row: IotCardImportTask) => {
|
||||||
const type = row.fail_count > 0 ? 'danger' : 'success'
|
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||||
return h(ElTag, { type, size: 'small' }, () => row.fail_count)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -225,37 +373,57 @@
|
|||||||
{
|
{
|
||||||
prop: 'started_at',
|
prop: 'started_at',
|
||||||
label: '开始时间',
|
label: '开始时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: (row: IotCardImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
|
formatter: (row: IotCardImportTask) => (row.started_at ? formatDateTime(row.started_at) : '-')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'completed_at',
|
prop: 'completed_at',
|
||||||
label: '完成时间',
|
label: '完成时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: (row: IotCardImportTask) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
|
formatter: (row: IotCardImportTask) =>
|
||||||
|
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'error_message',
|
prop: 'error_message',
|
||||||
label: '错误信息',
|
label: '错误信息',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
|
showOverflowTooltip: true,
|
||||||
formatter: (row: IotCardImportTask) => row.error_message || '-'
|
formatter: (row: IotCardImportTask) => row.error_message || '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'created_at',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
|
formatter: (row: IotCardImportTask) => formatDateTime(row.created_at)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 120,
|
width: 180,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: IotCardImportTask) => {
|
formatter: (row: IotCardImportTask) => {
|
||||||
return h(ArtButtonTable, {
|
const buttons = []
|
||||||
text: '查看详情',
|
|
||||||
|
// 显示"查看详情"按钮
|
||||||
|
buttons.push(
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '详情',
|
||||||
onClick: () => viewDetail(row)
|
onClick: () => viewDetail(row)
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果有失败数据,显示"失败数据"按钮
|
||||||
|
if (row.fail_count > 0) {
|
||||||
|
buttons.push(
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '失败数据',
|
||||||
|
type: 'danger',
|
||||||
|
onClick: () => downloadFailDataByRow(row)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -330,10 +498,202 @@
|
|||||||
pagination.page = newCurrentPage
|
pagination.page = newCurrentPage
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从行数据下载失败数据
|
||||||
|
const downloadFailDataByRow = async (row: IotCardImportTask) => {
|
||||||
|
try {
|
||||||
|
const res = await CardService.getIotCardImportTaskDetail(row.id)
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
const detail = res.data
|
||||||
|
downloadFailDataFromDetail(detail, row.task_no)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败数据失败:', error)
|
||||||
|
ElMessage.error('下载失败数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载失败数据(从详情对话框)
|
||||||
|
const downloadFailData = () => {
|
||||||
|
downloadFailDataFromDetail(currentDetail.value, currentDetail.value.task_no)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载失败数据的通用方法
|
||||||
|
const downloadFailDataFromDetail = (detail: any, taskNo: string) => {
|
||||||
|
const failReasons =
|
||||||
|
detail.failed_items?.map((item: any, index: number) => ({
|
||||||
|
row: index + 1,
|
||||||
|
iccid: item.iccid || '-',
|
||||||
|
msisdn: item.msisdn || '-',
|
||||||
|
message: item.reason || item.error || '未知错误'
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
if (failReasons.length === 0) {
|
||||||
|
ElMessage.warning('没有失败数据可下载')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = ['行号', 'ICCID', 'MSISDN', '失败原因']
|
||||||
|
const csvRows = [
|
||||||
|
headers.join(','),
|
||||||
|
...failReasons.map((item: any) =>
|
||||||
|
[item.row, item.iccid, item.msisdn, `"${item.message}"`].join(',')
|
||||||
|
)
|
||||||
|
]
|
||||||
|
const csvContent = csvRows.join('\n')
|
||||||
|
|
||||||
|
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', `IoT卡导入失败数据_${taskNo}.csv`)
|
||||||
|
link.style.visibility = 'hidden'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage.success('失败数据下载成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载模板
|
||||||
|
const downloadTemplate = () => {
|
||||||
|
const csvContent = [
|
||||||
|
'iccid,msisdn',
|
||||||
|
'89860123456789012345,13800138000',
|
||||||
|
'89860123456789012346,13800138001',
|
||||||
|
'89860123456789012347,13800138002'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
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', 'IoT卡导入模板.csv')
|
||||||
|
link.style.visibility = 'hidden'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
ElMessage.success('IoT卡导入模板下载成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件选择变化
|
||||||
|
const handleFileChange = (uploadFile: any) => {
|
||||||
|
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 = []
|
||||||
|
selectedCarrierId.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消导入
|
||||||
|
const handleCancelImport = () => {
|
||||||
|
clearFiles()
|
||||||
|
importDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交上传
|
||||||
|
const submitUpload = async () => {
|
||||||
|
if (!selectedCarrierId.value) {
|
||||||
|
ElMessage.warning('请先选择运营商')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileList.value.length) {
|
||||||
|
ElMessage.warning('请先选择CSV文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileList.value[0]
|
||||||
|
uploading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
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')
|
||||||
|
|
||||||
|
ElMessage.info('正在创建导入任务...')
|
||||||
|
const importRes = await CardService.importIotCards({
|
||||||
|
carrier_id: selectedCarrierId.value,
|
||||||
|
file_key,
|
||||||
|
batch_no: `IOT-${Date.now()}`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (importRes.code !== 0) {
|
||||||
|
throw new Error(importRes.msg || '创建导入任务失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskNo = importRes.data.task_no
|
||||||
|
|
||||||
|
handleCancelImport()
|
||||||
|
getTableData()
|
||||||
|
|
||||||
|
ElMessage.success({
|
||||||
|
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||||
|
duration: 3000,
|
||||||
|
showClose: true
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('IoT卡导入失败:', error)
|
||||||
|
ElMessage.error(error.message || 'IoT卡导入失败')
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.iot-card-task-page {
|
.iot-card-task-page {
|
||||||
// IoT card task page styles
|
: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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -62,18 +62,8 @@
|
|||||||
</ElDivider>
|
</ElDivider>
|
||||||
<ElTable :data="taskDetail.failed_items" border style="width: 100%">
|
<ElTable :data="taskDetail.failed_items" border style="width: 100%">
|
||||||
<ElTableColumn prop="line" label="行号" width="100" />
|
<ElTableColumn prop="line" label="行号" width="100" />
|
||||||
<ElTableColumn
|
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||||
v-if="taskType === 'card'"
|
<ElTableColumn v-else prop="device_no" label="设备号" min-width="180" />
|
||||||
prop="iccid"
|
|
||||||
label="ICCID"
|
|
||||||
min-width="180"
|
|
||||||
/>
|
|
||||||
<ElTableColumn
|
|
||||||
v-else
|
|
||||||
prop="device_no"
|
|
||||||
label="设备号"
|
|
||||||
min-width="180"
|
|
||||||
/>
|
|
||||||
<ElTableColumn prop="reason" label="失败原因" min-width="300" />
|
<ElTableColumn prop="reason" label="失败原因" min-width="300" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,18 +75,8 @@
|
|||||||
</ElDivider>
|
</ElDivider>
|
||||||
<ElTable :data="taskDetail.skipped_items" border style="width: 100%">
|
<ElTable :data="taskDetail.skipped_items" border style="width: 100%">
|
||||||
<ElTableColumn prop="line" label="行号" width="100" />
|
<ElTableColumn prop="line" label="行号" width="100" />
|
||||||
<ElTableColumn
|
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||||
v-if="taskType === 'card'"
|
<ElTableColumn v-else prop="device_no" label="设备号" min-width="180" />
|
||||||
prop="iccid"
|
|
||||||
label="ICCID"
|
|
||||||
min-width="180"
|
|
||||||
/>
|
|
||||||
<ElTableColumn
|
|
||||||
v-else
|
|
||||||
prop="device_no"
|
|
||||||
label="设备号"
|
|
||||||
min-width="180"
|
|
||||||
/>
|
|
||||||
<ElTableColumn prop="reason" label="跳过原因" min-width="300" />
|
<ElTableColumn prop="reason" label="跳过原因" min-width="300" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +88,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { CardService, DeviceService } from '@/api/modules'
|
import { CardService, DeviceService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag, ElDescriptions, ElDescriptionsItem, ElDivider, ElTable, ElTableColumn } from 'element-plus'
|
import {
|
||||||
|
ElMessage,
|
||||||
|
ElTag,
|
||||||
|
ElDescriptions,
|
||||||
|
ElDescriptionsItem,
|
||||||
|
ElDivider,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn
|
||||||
|
} from 'element-plus'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
import type { IotCardImportTaskDetail, IotCardImportTaskStatus } from '@/types/api/card'
|
||||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||||
|
|||||||
@@ -15,8 +15,14 @@
|
|||||||
<p>1. 请先下载 CSV 模板文件,按照模板格式填写设备信息</p>
|
<p>1. 请先下载 CSV 模板文件,按照模板格式填写设备信息</p>
|
||||||
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
<p>2. 支持 CSV 格式(.csv),单次最多导入 1000 条</p>
|
||||||
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
<p>3. CSV 文件编码:UTF-8(推荐)或 GBK</p>
|
||||||
<p>4. 必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p>
|
<p
|
||||||
<p>5. 可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p>
|
>4.
|
||||||
|
必填字段:device_no(设备号)、device_name(设备名称)、device_model(设备型号)</p
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
>5.
|
||||||
|
可选字段:device_type(设备类型)、manufacturer(制造商)、max_sim_slots(最大插槽数,默认1)</p
|
||||||
|
>
|
||||||
<p>6. 设备号重复将自动跳过,导入后可在任务管理中查看详情</p>
|
<p>6. 设备号重复将自动跳过,导入后可在任务管理中查看详情</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,52 +65,6 @@
|
|||||||
</ElRow>
|
</ElRow>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<!-- 导入统计 -->
|
|
||||||
<ElRow :gutter="20" style="margin-top: 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">1,250</div>
|
|
||||||
</div>
|
|
||||||
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Upload /></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)">1,180</div>
|
|
||||||
</div>
|
|
||||||
<el-icon class="stat-icon" style="color: var(--el-color-success)"
|
|
||||||
><SuccessFilled
|
|
||||||
/></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)">70</div>
|
|
||||||
</div>
|
|
||||||
<el-icon class="stat-icon" style="color: var(--el-color-danger)"
|
|
||||||
><CircleCloseFilled
|
|
||||||
/></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">94.4%</div>
|
|
||||||
</div>
|
|
||||||
<el-icon class="stat-icon" style="color: var(--el-color-warning)"
|
|
||||||
><TrendCharts
|
|
||||||
/></el-icon>
|
|
||||||
</ElCard>
|
|
||||||
</ElCol>
|
|
||||||
</ElRow>
|
|
||||||
|
|
||||||
<!-- 导入记录 -->
|
<!-- 导入记录 -->
|
||||||
<ElCard shadow="never" style="margin-top: 20px">
|
<ElCard shadow="never" style="margin-top: 20px">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -117,71 +77,27 @@
|
|||||||
style="width: 120px; margin-right: 12px"
|
style="width: 120px; margin-right: 12px"
|
||||||
clearable
|
clearable
|
||||||
>
|
>
|
||||||
<ElOption label="全部" value="" />
|
<ElOption label="全部" :value="null" />
|
||||||
<ElOption label="处理中" value="processing" />
|
<ElOption label="待处理" :value="1" />
|
||||||
<ElOption label="完成" value="success" />
|
<ElOption label="处理中" :value="2" />
|
||||||
<ElOption label="失败" value="failed" />
|
<ElOption label="已完成" :value="3" />
|
||||||
|
<ElOption label="失败" :value="4" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElButton @click="refreshList">刷新</ElButton>
|
<ElButton @click="refreshList">刷新</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ArtTable :data="filteredRecords" index>
|
<ArtTable
|
||||||
<template #default>
|
rowKey="id"
|
||||||
<ElTableColumn label="导入批次号" prop="batchNo" width="180" />
|
ref="tableRef"
|
||||||
<ElTableColumn label="文件名" prop="fileName" min-width="200" show-overflow-tooltip />
|
:loading="loading"
|
||||||
<ElTableColumn label="设备总数" prop="totalCount" width="100" />
|
:data="filteredRecords"
|
||||||
<ElTableColumn label="成功数" prop="successCount" width="100">
|
:marginTop="10"
|
||||||
<template #default="scope">
|
:stripe="false"
|
||||||
<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="已绑定ICCID" prop="bindCount" width="120">
|
|
||||||
<template #default="scope">
|
|
||||||
<ElTag type="success" size="small">{{ scope.row.bindCount }}</ElTag>
|
|
||||||
</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="200">
|
|
||||||
<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)"
|
|
||||||
>
|
>
|
||||||
失败数据
|
<template #default>
|
||||||
</el-button>
|
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</template>
|
</template>
|
||||||
</ArtTable>
|
</ArtTable>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
@@ -189,20 +105,31 @@
|
|||||||
<!-- 导入详情对话框 -->
|
<!-- 导入详情对话框 -->
|
||||||
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
|
<ElDialog v-model="detailDialogVisible" title="设备导入详情" width="900px" align-center>
|
||||||
<ElDescriptions :column="2" border>
|
<ElDescriptions :column="2" border>
|
||||||
|
<ElDescriptionsItem label="任务编号" :span="2">{{
|
||||||
|
currentDetail.taskNo
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="状态">{{ currentDetail.statusText }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="批次号">{{ currentDetail.batchNo }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="文件名">{{ currentDetail.fileName }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="文件名" :span="2">{{
|
||||||
<ElDescriptionsItem label="设备总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
currentDetail.fileName
|
||||||
<ElDescriptionsItem label="成功导入">
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总数">{{ currentDetail.totalCount }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="成功数">
|
||||||
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
<span style="color: var(--el-color-success)">{{ currentDetail.successCount }}</span>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="导入失败">
|
<ElDescriptionsItem label="跳过数">{{ currentDetail.skipCount }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="失败数">
|
||||||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="已绑定ICCID">
|
<ElDescriptionsItem label="警告数">
|
||||||
<ElTag type="success">{{ currentDetail.bindCount }}</ElTag>
|
<span style="color: var(--el-color-warning)">{{ currentDetail.warningCount }}</span>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="导入时间">{{ currentDetail.importTime }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="开始时间">{{ currentDetail.startedAt }}</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="操作人">{{ currentDetail.operator }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="完成时间">{{ currentDetail.completedAt }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="创建时间">{{ currentDetail.createdAt }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="错误信息" :span="2">{{
|
||||||
|
currentDetail.errorMessage
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<ElDivider content-position="left">失败明细</ElDivider>
|
<ElDivider content-position="left">失败明细</ElDivider>
|
||||||
@@ -235,22 +162,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { h, computed, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElTag, ElProgress, ElIcon, ElButton } from 'element-plus'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import { Download, UploadFilled, View, Loading } from '@element-plus/icons-vue'
|
||||||
Download,
|
|
||||||
UploadFilled,
|
|
||||||
View,
|
|
||||||
Loading,
|
|
||||||
Upload,
|
|
||||||
SuccessFilled,
|
|
||||||
CircleCloseFilled,
|
|
||||||
TrendCharts
|
|
||||||
} from '@element-plus/icons-vue'
|
|
||||||
import type { UploadInstance } from 'element-plus'
|
import type { UploadInstance } from 'element-plus'
|
||||||
import { StorageService } from '@/api/modules/storage'
|
import { StorageService } from '@/api/modules/storage'
|
||||||
import { DeviceService } from '@/api/modules'
|
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' })
|
defineOptions({ name: 'DeviceImport' })
|
||||||
|
|
||||||
@@ -265,107 +186,213 @@
|
|||||||
|
|
||||||
interface ImportRecord {
|
interface ImportRecord {
|
||||||
id: string
|
id: string
|
||||||
|
taskNo: string
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
batchNo: string
|
batchNo: string
|
||||||
fileName: string
|
fileName: string
|
||||||
totalCount: number
|
totalCount: number
|
||||||
successCount: number
|
successCount: number
|
||||||
|
skipCount: number
|
||||||
failCount: number
|
failCount: number
|
||||||
bindCount: number
|
warningCount: number
|
||||||
status: 'pending' | 'processing' | 'success' | 'failed'
|
startedAt: string
|
||||||
progress: number
|
completedAt: string
|
||||||
importTime: string
|
errorMessage: string
|
||||||
operator: string
|
createdAt: string
|
||||||
failReasons?: FailReason[]
|
failReasons?: FailReason[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadRef = ref<UploadInstance>()
|
const uploadRef = ref<UploadInstance>()
|
||||||
|
const tableRef = ref()
|
||||||
const fileList = ref<File[]>([])
|
const fileList = ref<File[]>([])
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref<number | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
const importRecords = ref<ImportRecord[]>([
|
const importRecords = ref<ImportRecord[]>([])
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
batchNo: 'DEV20260109001',
|
|
||||||
fileName: '设备导入模板_20260109.xlsx',
|
|
||||||
totalCount: 300,
|
|
||||||
successCount: 285,
|
|
||||||
failCount: 15,
|
|
||||||
bindCount: 285,
|
|
||||||
status: 'success',
|
|
||||||
progress: 100,
|
|
||||||
importTime: '2026-01-09 11:30:00',
|
|
||||||
operator: 'admin',
|
|
||||||
failReasons: [
|
|
||||||
{ row: 12, deviceCode: 'DEV001', iccid: '89860123456789012345', message: 'ICCID 不存在' },
|
|
||||||
{ row: 23, deviceCode: 'DEV002', iccid: '89860123456789012346', message: '设备编号已存在' },
|
|
||||||
{ row: 45, deviceCode: '', iccid: '89860123456789012347', message: '设备编号为空' },
|
|
||||||
{ row: 67, deviceCode: 'DEV003', iccid: '', message: 'ICCID 为空' },
|
|
||||||
{ row: 89, deviceCode: 'DEV004', iccid: '89860123456789012348', message: '设备类型不存在' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
batchNo: 'DEV20260108001',
|
|
||||||
fileName: '智能水表设备批量导入.xlsx',
|
|
||||||
totalCount: 150,
|
|
||||||
successCount: 150,
|
|
||||||
failCount: 0,
|
|
||||||
bindCount: 150,
|
|
||||||
status: 'success',
|
|
||||||
progress: 100,
|
|
||||||
importTime: '2026-01-08 14:20:00',
|
|
||||||
operator: 'admin'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
batchNo: 'DEV20260107001',
|
|
||||||
fileName: 'GPS定位器导入.xlsx',
|
|
||||||
totalCount: 200,
|
|
||||||
successCount: 180,
|
|
||||||
failCount: 20,
|
|
||||||
bindCount: 180,
|
|
||||||
status: 'success',
|
|
||||||
progress: 100,
|
|
||||||
importTime: '2026-01-07 10:15:00',
|
|
||||||
operator: 'operator01',
|
|
||||||
failReasons: [
|
|
||||||
{
|
|
||||||
row: 10,
|
|
||||||
deviceCode: 'GPS001',
|
|
||||||
iccid: '89860123456789012349',
|
|
||||||
message: 'ICCID 已被其他设备绑定'
|
|
||||||
},
|
|
||||||
{ row: 20, deviceCode: 'GPS002', iccid: '89860123456789012350', message: 'ICCID 状态异常' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const currentDetail = ref<ImportRecord>({
|
const currentDetail = ref<ImportRecord>({
|
||||||
id: '',
|
id: '',
|
||||||
|
taskNo: '',
|
||||||
|
status: 1,
|
||||||
|
statusText: '',
|
||||||
batchNo: '',
|
batchNo: '',
|
||||||
fileName: '',
|
fileName: '',
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
successCount: 0,
|
successCount: 0,
|
||||||
|
skipCount: 0,
|
||||||
failCount: 0,
|
failCount: 0,
|
||||||
bindCount: 0,
|
warningCount: 0,
|
||||||
status: 'pending',
|
startedAt: '',
|
||||||
progress: 0,
|
completedAt: '',
|
||||||
importTime: '',
|
errorMessage: '',
|
||||||
operator: ''
|
createdAt: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredRecords = computed(() => {
|
const filteredRecords = computed(() => {
|
||||||
if (!statusFilter.value) return importRecords.value
|
if (statusFilter.value === null) return importRecords.value
|
||||||
return importRecords.value.filter((item) => item.status === statusFilter.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 = () => {
|
const downloadTemplate = () => {
|
||||||
ElMessage.success('模板下载中...')
|
// CSV模板内容 - 包含表头和示例数据
|
||||||
setTimeout(() => {
|
const csvContent = [
|
||||||
|
// 表头
|
||||||
|
'device_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('设备导入模板下载成功')
|
ElMessage.success('设备导入模板下载成功')
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileChange = (uploadFile: any) => {
|
const handleFileChange = (uploadFile: any) => {
|
||||||
@@ -444,17 +471,15 @@
|
|||||||
// 清空文件列表
|
// 清空文件列表
|
||||||
clearFiles()
|
clearFiles()
|
||||||
|
|
||||||
// 显示成功消息并提供跳转链接
|
// 刷新任务列表
|
||||||
|
await fetchImportTasks()
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
ElMessage.success({
|
ElMessage.success({
|
||||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||||
duration: 5000,
|
duration: 3000,
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3秒后跳转到任务管理页面
|
|
||||||
setTimeout(() => {
|
|
||||||
router.push('/asset-management/task-management')
|
|
||||||
}, 3000)
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('设备导入失败:', error)
|
console.error('设备导入失败:', error)
|
||||||
ElMessage.error(error.message || '设备导入失败')
|
ElMessage.error(error.message || '设备导入失败')
|
||||||
@@ -463,21 +488,133 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshList = () => {
|
// 获取导入任务列表
|
||||||
ElMessage.success('刷新成功')
|
const fetchImportTasks = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page: 1,
|
||||||
|
page_size: 100
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewDetail = (row: ImportRecord) => {
|
// 如果有状态筛选,添加到参数
|
||||||
currentDetail.value = { ...row }
|
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.device_no || '-',
|
||||||
|
iccid: item.iccid || '-',
|
||||||
|
message: item.reason || item.error || '未知错误'
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
detailDialogVisible.value = true
|
detailDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error)
|
||||||
|
ElMessage.error('获取任务详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const downloadFailData = (row: ImportRecord) => {
|
const downloadFailData = (row: ImportRecord) => {
|
||||||
ElMessage.info(`正在下载批次 ${row.batchNo} 的失败数据...`)
|
if (!row.failReasons || row.failReasons.length === 0) {
|
||||||
setTimeout(() => {
|
ElMessage.warning('没有失败数据可下载')
|
||||||
ElMessage.success('失败数据下载完成')
|
return
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成失败数据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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -125,7 +125,12 @@
|
|||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="订单号" prop="order_no" min-width="180" show-overflow-tooltip />
|
<ElTableColumn label="订单号" prop="order_no" min-width="180" show-overflow-tooltip />
|
||||||
<ElTableColumn label="ICCID" prop="iccid" min-width="150" show-overflow-tooltip />
|
<ElTableColumn label="ICCID" prop="iccid" min-width="150" show-overflow-tooltip />
|
||||||
<ElTableColumn label="设备号" prop="device_no" min-width="150" show-overflow-tooltip />
|
<ElTableColumn
|
||||||
|
label="设备号"
|
||||||
|
prop="device_no"
|
||||||
|
min-width="150"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
<ElTableColumn label="入账时间" prop="created_at" width="180">
|
<ElTableColumn label="入账时间" prop="created_at" width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDateTime(scope.row.created_at) }}
|
{{ formatDateTime(scope.row.created_at) }}
|
||||||
|
|||||||
@@ -54,10 +54,7 @@
|
|||||||
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
|
:placeholder="t('orderManagement.searchForm.orderTypePlaceholder')"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption :label="t('orderManagement.orderType.singleCard')" value="single_card" />
|
||||||
:label="t('orderManagement.orderType.singleCard')"
|
|
||||||
value="single_card"
|
|
||||||
/>
|
|
||||||
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
|
<ElOption :label="t('orderManagement.orderType.device')" value="device" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -154,7 +151,10 @@
|
|||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<!-- 订单项列表 -->
|
<!-- 订单项列表 -->
|
||||||
<div v-if="currentOrder.items && currentOrder.items.length > 0" style="margin-top: 20px">
|
<div
|
||||||
|
v-if="currentOrder.items && currentOrder.items.length > 0"
|
||||||
|
style="margin-top: 20px"
|
||||||
|
>
|
||||||
<h4>{{ t('orderManagement.orderItems') }}</h4>
|
<h4>{{ t('orderManagement.orderItems') }}</h4>
|
||||||
<ElTable :data="currentOrder.items" border style="margin-top: 10px">
|
<ElTable :data="currentOrder.items" border style="margin-top: 10px">
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
@@ -176,11 +176,7 @@
|
|||||||
{{ formatCurrency(row.unit_price) }}
|
{{ formatCurrency(row.unit_price) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn
|
<ElTableColumn prop="amount" :label="t('orderManagement.items.amount')" width="120">
|
||||||
prop="amount"
|
|
||||||
:label="t('orderManagement.items.amount')"
|
|
||||||
width="120"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatCurrency(row.amount) }}
|
{{ formatCurrency(row.amount) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -415,10 +411,8 @@
|
|||||||
label: t('orderManagement.table.orderType'),
|
label: t('orderManagement.table.orderType'),
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: (row: Order) => {
|
formatter: (row: Order) => {
|
||||||
return h(
|
return h(ElTag, { type: row.order_type === 'single_card' ? 'primary' : 'success' }, () =>
|
||||||
ElTag,
|
getOrderTypeText(row.order_type)
|
||||||
{ type: row.order_type === 'single_card' ? 'primary' : 'success' },
|
|
||||||
() => getOrderTypeText(row.order_type)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -427,10 +421,8 @@
|
|||||||
label: t('orderManagement.table.buyerType'),
|
label: t('orderManagement.table.buyerType'),
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: (row: Order) => {
|
formatter: (row: Order) => {
|
||||||
return h(
|
return h(ElTag, { type: row.buyer_type === 'personal' ? 'info' : 'warning' }, () =>
|
||||||
ElTag,
|
getBuyerTypeText(row.buyer_type)
|
||||||
{ type: row.buyer_type === 'personal' ? 'info' : 'warning' },
|
|
||||||
() => getBuyerTypeText(row.buyer_type)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -439,8 +431,10 @@
|
|||||||
label: t('orderManagement.table.paymentStatus'),
|
label: t('orderManagement.table.paymentStatus'),
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: (row: Order) => {
|
formatter: (row: Order) => {
|
||||||
return h(ElTag, { type: getPaymentStatusType(row.payment_status) }, () =>
|
return h(
|
||||||
row.payment_status_text
|
ElTag,
|
||||||
|
{ type: getPaymentStatusType(row.payment_status) },
|
||||||
|
() => row.payment_status_text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,12 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@closed="handleCostPriceDialogClosed"
|
@closed="handleCostPriceDialogClosed"
|
||||||
>
|
>
|
||||||
<ElForm ref="costPriceFormRef" :model="costPriceForm" :rules="costPriceRules" label-width="120px">
|
<ElForm
|
||||||
|
ref="costPriceFormRef"
|
||||||
|
:model="costPriceForm"
|
||||||
|
:rules="costPriceRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
<ElFormItem label="套餐名称">
|
<ElFormItem label="套餐名称">
|
||||||
<ElInput v-model="costPriceForm.package_name" disabled />
|
<ElInput v-model="costPriceForm.package_name" disabled />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
@@ -56,7 +61,12 @@
|
|||||||
<ElInput v-model="costPriceForm.shop_name" disabled />
|
<ElInput v-model="costPriceForm.shop_name" disabled />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="原成本价(分)">
|
<ElFormItem label="原成本价(分)">
|
||||||
<ElInputNumber v-model="costPriceForm.old_cost_price" disabled :controls="false" style="width: 100%" />
|
<ElInputNumber
|
||||||
|
v-model="costPriceForm.old_cost_price"
|
||||||
|
disabled
|
||||||
|
:controls="false"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="新成本价(分)" prop="cost_price">
|
<ElFormItem label="新成本价(分)" prop="cost_price">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
@@ -71,7 +81,11 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<ElButton @click="costPriceDialogVisible = false">取消</ElButton>
|
<ElButton @click="costPriceDialogVisible = false">取消</ElButton>
|
||||||
<ElButton type="primary" @click="handleCostPriceSubmit(costPriceFormRef)" :loading="costPriceSubmitLoading">
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
@click="handleCostPriceSubmit(costPriceFormRef)"
|
||||||
|
:loading="costPriceSubmitLoading"
|
||||||
|
>
|
||||||
提交
|
提交
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,11 +168,7 @@
|
|||||||
import { ShopPackageAllocationService, PackageManageService, ShopService } from '@/api/modules'
|
import { ShopPackageAllocationService, PackageManageService, ShopService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElSwitch } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type {
|
import type { ShopPackageAllocationResponse, PackageResponse, ShopResponse } from '@/types/api'
|
||||||
ShopPackageAllocationResponse,
|
|
||||||
PackageResponse,
|
|
||||||
ShopResponse
|
|
||||||
} from '@/types/api'
|
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
@@ -281,7 +291,9 @@
|
|||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
callback(new Error('请输入成本价'))
|
callback(new Error('请输入成本价'))
|
||||||
} else if (form.package_base_price && value < form.package_base_price) {
|
} else if (form.package_base_price && value < form.package_base_price) {
|
||||||
callback(new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`))
|
callback(
|
||||||
|
new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
@@ -355,7 +367,8 @@
|
|||||||
prop: 'calculated_cost_price',
|
prop: 'calculated_cost_price',
|
||||||
label: '原计算成本价',
|
label: '原计算成本价',
|
||||||
width: 120,
|
width: 120,
|
||||||
formatter: (row: ShopPackageAllocationResponse) => `¥${(row.calculated_cost_price / 100).toFixed(2)}`
|
formatter: (row: ShopPackageAllocationResponse) =>
|
||||||
|
`¥${(row.calculated_cost_price / 100).toFixed(2)}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -624,7 +637,7 @@
|
|||||||
const handlePackageChange = (packageId: number | undefined) => {
|
const handlePackageChange = (packageId: number | undefined) => {
|
||||||
if (packageId) {
|
if (packageId) {
|
||||||
// 从套餐选项中找到选中的套餐
|
// 从套餐选项中找到选中的套餐
|
||||||
const selectedPackage = packageOptions.value.find(pkg => pkg.id === packageId)
|
const selectedPackage = packageOptions.value.find((pkg) => pkg.id === packageId)
|
||||||
if (selectedPackage) {
|
if (selectedPackage) {
|
||||||
// 将套餐的价格设置为成本价
|
// 将套餐的价格设置为成本价
|
||||||
form.cost_price = selectedPackage.price
|
form.cost_price = selectedPackage.price
|
||||||
|
|||||||
@@ -94,11 +94,7 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="流量类型" prop="data_type">
|
<ElFormItem label="流量类型" prop="data_type">
|
||||||
<ElSelect
|
<ElSelect v-model="form.data_type" placeholder="请选择流量类型" style="width: 100%">
|
||||||
v-model="form.data_type"
|
|
||||||
placeholder="请选择流量类型"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="option in DATA_TYPE_OPTIONS"
|
v-for="option in DATA_TYPE_OPTIONS"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
@@ -116,7 +112,11 @@
|
|||||||
placeholder="请输入真流量额度"
|
placeholder="请输入真流量额度"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="虚流量额度(MB)" prop="virtual_data_mb" v-if="form.data_type === 'virtual'">
|
<ElFormItem
|
||||||
|
label="虚流量额度(MB)"
|
||||||
|
prop="virtual_data_mb"
|
||||||
|
v-if="form.data_type === 'virtual'"
|
||||||
|
>
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="form.virtual_data_mb"
|
v-model="form.virtual_data_mb"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -135,12 +135,7 @@
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="价格(分)" prop="price">
|
<ElFormItem label="价格(分)" prop="price">
|
||||||
<ElInputNumber
|
<ElInputNumber v-model="form.price" :min="0" :controls="false" style="width: 100%" />
|
||||||
v-model="form.price"
|
|
||||||
:min="0"
|
|
||||||
:controls="false"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="套餐描述" prop="description">
|
<ElFormItem label="套餐描述" prop="description">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -387,10 +382,8 @@
|
|||||||
label: '套餐类型',
|
label: '套餐类型',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: PackageResponse) => {
|
formatter: (row: PackageResponse) => {
|
||||||
return h(
|
return h(ElTag, { type: getPackageTypeTag(row.package_type), size: 'small' }, () =>
|
||||||
ElTag,
|
getPackageTypeLabel(row.package_type)
|
||||||
{ type: getPackageTypeTag(row.package_type), size: 'small' },
|
|
||||||
() => getPackageTypeLabel(row.package_type)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -399,10 +392,8 @@
|
|||||||
label: '流量类型',
|
label: '流量类型',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: PackageResponse) => {
|
formatter: (row: PackageResponse) => {
|
||||||
return h(
|
return h(ElTag, { type: getDataTypeTag(row.data_type), size: 'small' }, () =>
|
||||||
ElTag,
|
getDataTypeLabel(row.data_type)
|
||||||
{ type: getDataTypeTag(row.data_type), size: 'small' },
|
|
||||||
() => getDataTypeLabel(row.data_type)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -176,7 +176,11 @@
|
|||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
:label="form.one_time_commission_config.mode === 'fixed' ? '佣金金额(分)' : '佣金比例(千分比)'"
|
:label="
|
||||||
|
form.one_time_commission_config.mode === 'fixed'
|
||||||
|
? '佣金金额(分)'
|
||||||
|
: '佣金比例(千分比)'
|
||||||
|
"
|
||||||
prop="one_time_commission_config.value"
|
prop="one_time_commission_config.value"
|
||||||
>
|
>
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
@@ -197,8 +201,16 @@
|
|||||||
<template v-if="form.one_time_commission_config.type === 'tiered'">
|
<template v-if="form.one_time_commission_config.type === 'tiered'">
|
||||||
<ElFormItem label="梯度档位">
|
<ElFormItem label="梯度档位">
|
||||||
<div class="tier-list">
|
<div class="tier-list">
|
||||||
<div v-for="(tier, index) in form.one_time_commission_config.tiers" :key="index" class="tier-item">
|
<div
|
||||||
<ElSelect v-model="tier.tier_type" placeholder="梯度类型" style="width: 120px">
|
v-for="(tier, index) in form.one_time_commission_config.tiers"
|
||||||
|
:key="index"
|
||||||
|
class="tier-item"
|
||||||
|
>
|
||||||
|
<ElSelect
|
||||||
|
v-model="tier.tier_type"
|
||||||
|
placeholder="梯度类型"
|
||||||
|
style="width: 120px"
|
||||||
|
>
|
||||||
<ElOption label="销量" value="sales_count" />
|
<ElOption label="销量" value="sales_count" />
|
||||||
<ElOption label="销售额" value="sales_amount" />
|
<ElOption label="销售额" value="sales_amount" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
@@ -903,12 +915,14 @@
|
|||||||
}
|
}
|
||||||
// 梯度类型配置
|
// 梯度类型配置
|
||||||
else if (form.one_time_commission_config.type === 'tiered') {
|
else if (form.one_time_commission_config.type === 'tiered') {
|
||||||
data.one_time_commission_config.tiers = form.one_time_commission_config.tiers.map((t: any) => ({
|
data.one_time_commission_config.tiers = form.one_time_commission_config.tiers.map(
|
||||||
|
(t: any) => ({
|
||||||
tier_type: t.tier_type,
|
tier_type: t.tier_type,
|
||||||
threshold: t.threshold,
|
threshold: t.threshold,
|
||||||
mode: t.mode,
|
mode: t.mode,
|
||||||
value: t.value
|
value: t.value
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,9 +976,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-tip {
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tier-list {
|
.tier-list {
|
||||||
@@ -981,26 +995,26 @@
|
|||||||
.info-row {
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 18px;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
font-size: 14px;
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
font-size: 14px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
.info-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--art-primary);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: var(--art-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -455,7 +455,8 @@
|
|||||||
activeText: getStatusText(CommonStatus.ENABLED),
|
activeText: getStatusText(CommonStatus.ENABLED),
|
||||||
inactiveText: getStatusText(CommonStatus.DISABLED),
|
inactiveText: getStatusText(CommonStatus.DISABLED),
|
||||||
inlinePrompt: true,
|
inlinePrompt: true,
|
||||||
'onUpdate:modelValue': (val: string | number | boolean) => handleStatusChange(row, val as number)
|
'onUpdate:modelValue': (val: string | number | boolean) =>
|
||||||
|
handleStatusChange(row, val as number)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,12 @@
|
|||||||
<ElInput v-model="form.perm_name" placeholder="请输入权限名称" />
|
<ElInput v-model="form.perm_name" placeholder="请输入权限名称" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="权限标识" prop="perm_code">
|
<ElFormItem label="权限标识" prop="perm_code">
|
||||||
<ElInput v-model="form.perm_code" placeholder="请输入权限标识,如:user:add" />
|
<ElInput
|
||||||
|
v-model="form.perm_code"
|
||||||
|
placeholder="例如:菜单权限:menu:role 按钮权限: role:add"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="权限类型" prop="perm_type">
|
<ElFormItem label="权限类型" prop="perm_type">
|
||||||
<ElSelect v-model="form.perm_type" placeholder="请选择权限类型" style="width: 100%">
|
<ElSelect v-model="form.perm_type" placeholder="请选择权限类型" style="width: 100%">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
@refresh="handleRefresh"
|
@refresh="handleRefresh"
|
||||||
>
|
>
|
||||||
<template #left>
|
<template #left>
|
||||||
<ElButton @click="showDialog('add')">新增角色</ElButton>
|
<ElButton @click="showDialog('add')" v-permission="'role:add'">新增角色</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
@@ -59,7 +59,12 @@
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="角色类型" prop="role_type">
|
<ElFormItem label="角色类型" prop="role_type">
|
||||||
<ElSelect v-model="form.role_type" placeholder="请选择角色类型" style="width: 100%">
|
<ElSelect
|
||||||
|
v-model="form.role_type"
|
||||||
|
placeholder="请选择角色类型"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
>
|
||||||
<ElOption label="平台角色" :value="1" />
|
<ElOption label="平台角色" :value="1" />
|
||||||
<ElOption label="客户角色" :value="2" />
|
<ElOption label="客户角色" :value="2" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
@@ -226,13 +231,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'role_desc',
|
prop: 'role_desc',
|
||||||
label: '角色描述',
|
label: '角色描述'
|
||||||
minWidth: 150
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'role_type',
|
prop: 'role_type',
|
||||||
label: '角色类型',
|
label: '角色类型',
|
||||||
width: 100,
|
minWidth: 120,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
|
return h(ElTag, { type: row.role_type === 1 ? 'primary' : 'success' }, () =>
|
||||||
row.role_type === 1 ? '平台角色' : '客户角色'
|
row.role_type === 1 ? '平台角色' : '客户角色'
|
||||||
@@ -242,7 +246,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
width: 100,
|
minWidth: 100,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
return h(ElSwitch, {
|
return h(ElSwitch, {
|
||||||
modelValue: row.status,
|
modelValue: row.status,
|
||||||
@@ -259,13 +263,13 @@
|
|||||||
{
|
{
|
||||||
prop: 'CreatedAt',
|
prop: 'CreatedAt',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
width: 180,
|
minWidth: 180,
|
||||||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 180,
|
width: 200,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
@@ -481,17 +485,22 @@
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
const data = {
|
const data = {
|
||||||
role_name: form.role_name,
|
role_name: form.role_name,
|
||||||
role_desc: form.role_desc,
|
role_desc: form.role_desc,
|
||||||
role_type: form.role_type,
|
role_type: form.role_type,
|
||||||
status: form.status
|
status: form.status
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogType.value === 'add') {
|
|
||||||
await RoleService.createRole(data)
|
await RoleService.createRole(data)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
} else {
|
} else {
|
||||||
|
// 更新角色时只发送允许的字段
|
||||||
|
const data = {
|
||||||
|
role_name: form.role_name,
|
||||||
|
role_desc: form.role_desc,
|
||||||
|
status: form.status
|
||||||
|
}
|
||||||
await RoleService.updateRole(form.id, data)
|
await RoleService.updateRole(form.id, data)
|
||||||
ElMessage.success('修改成功')
|
ElMessage.success('修改成功')
|
||||||
}
|
}
|
||||||
@@ -522,5 +531,4 @@
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user