fetch(modify):修复BUG
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m27s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m27s
This commit is contained in:
@@ -310,6 +310,12 @@
|
|||||||
"whenever": true,
|
"whenever": true,
|
||||||
"ElMessage": true,
|
"ElMessage": true,
|
||||||
"ElTag": true,
|
"ElTag": true,
|
||||||
"ElMessageBox": true
|
"ElMessageBox": true,
|
||||||
|
"ElButton": true,
|
||||||
|
"ElDropdown": true,
|
||||||
|
"ElButton2": true,
|
||||||
|
"ElDropdown2": true,
|
||||||
|
"ElDropdownItem": true,
|
||||||
|
"ElDropdownMenu": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
openspec/changes/add-iot-device-operations/proposal.md
Normal file
56
openspec/changes/add-iot-device-operations/proposal.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Change: Add IoT Card and Device Operations
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
运营人员和代理商需要更丰富的物联网卡和设备管理功能,包括查询流量使用情况、实名认证状态、卡片状态、启停操作,以及设备的重启、重置、限速、换卡、WiFi设置等操作。这些功能是物联网卡全生命周期管理的核心能力。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### IoT Card Operations (6 new APIs)
|
||||||
|
- 查询流量使用情况 (GET /api/admin/iot-cards/{iccid}/gateway-flow)
|
||||||
|
- 查询实名认证状态 (GET /api/admin/iot-cards/{iccid}/gateway-realname)
|
||||||
|
- 查询卡片状态 (GET /api/admin/iot-cards/{iccid}/gateway-status)
|
||||||
|
- 获取实名认证链接 (GET /api/admin/iot-cards/{iccid}/realname-link)
|
||||||
|
- 启用物联网卡 (POST /api/admin/iot-cards/{iccid}/start)
|
||||||
|
- 停用物联网卡 (POST /api/admin/iot-cards/{iccid}/stop)
|
||||||
|
|
||||||
|
### Device Operations (6 new APIs)
|
||||||
|
- 重启设备 (POST /api/admin/devices/by-imei/{imei}/reboot)
|
||||||
|
- 重置设备 (POST /api/admin/devices/by-imei/{imei}/reset)
|
||||||
|
- 设置限速 (PUT /api/admin/devices/by-imei/{imei}/speed-limit)
|
||||||
|
- 切换SIM卡 (POST /api/admin/devices/by-imei/{imei}/switch-card)
|
||||||
|
- 设置WiFi (PUT /api/admin/devices/by-imei/{imei}/wifi)
|
||||||
|
|
||||||
|
### UI Changes
|
||||||
|
- 在 `/asset-management/iot-card-management` 页面添加操作按钮:
|
||||||
|
- "查询流量"按钮(主要操作)
|
||||||
|
- "更多操作"下拉菜单(包含其他5个操作)
|
||||||
|
- 在 `/asset-management/devices` 页面添加操作按钮:
|
||||||
|
- "重启设备"按钮(主要操作)
|
||||||
|
- "更多操作"下拉菜单(包含其他5个操作)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
### Affected Specs
|
||||||
|
- **NEW**: `specs/iot-card-operations/spec.md` - IoT卡操作相关的所有需求
|
||||||
|
- **NEW**: `specs/device-operations/spec.md` - 设备操作相关的所有需求
|
||||||
|
|
||||||
|
### Affected Code
|
||||||
|
- **API层**:
|
||||||
|
- 新增 `src/api/modules/iotCard.ts` - IoT卡操作API方法
|
||||||
|
- 新增 `src/api/modules/device.ts` - 设备操作API方法
|
||||||
|
- **类型定义**:
|
||||||
|
- 新增 `src/types/api/iotCard.ts` - IoT卡相关类型
|
||||||
|
- 新增 `src/types/api/device.ts` - 设备相关类型
|
||||||
|
- **页面组件**:
|
||||||
|
- 修改 `src/views/asset-management/iot-card-management/index.vue` - 添加操作按钮和对话框
|
||||||
|
- 修改 `src/views/asset-management/devices/index.vue` - 添加操作按钮和对话框
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
无破坏性变更。所有变更都是增量式的新功能添加。
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- 后端API已实现(见 `docs/2-3新增接口.md`)
|
||||||
|
- Element Plus UI组件库(已在项目中)
|
||||||
|
- Axios HTTP客户端(已在项目中)
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# Device Operations Specification
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Reboot Device
|
||||||
|
|
||||||
|
The system SHALL provide the ability to reboot an IoT device via its IMEI.
|
||||||
|
|
||||||
|
#### Scenario: Successfully reboot device
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a reboot operation on a device with valid IMEI
|
||||||
|
- **THEN** the system sends the reboot command to the device
|
||||||
|
- **AND** returns a success response with operation confirmation
|
||||||
|
|
||||||
|
#### Scenario: Reboot operation with confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the reboot device button
|
||||||
|
- **THEN** the system prompts for confirmation with a warning message
|
||||||
|
- **AND** only proceeds if the user confirms the action
|
||||||
|
|
||||||
|
#### Scenario: Reboot with invalid IMEI
|
||||||
|
|
||||||
|
- **WHEN** a user attempts to reboot a device with invalid or non-existent IMEI
|
||||||
|
- **THEN** the system returns a 400 error with appropriate error message
|
||||||
|
|
||||||
|
### Requirement: Reset Device to Factory Settings
|
||||||
|
|
||||||
|
The system SHALL provide the ability to reset an IoT device to factory settings via its IMEI.
|
||||||
|
|
||||||
|
#### Scenario: Successfully reset device
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a factory reset operation on a device
|
||||||
|
- **THEN** the system sends the reset command to the device
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Reset operation with strong confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the factory reset button
|
||||||
|
- **THEN** the system displays a warning dialog explaining data loss
|
||||||
|
- **AND** requires user confirmation before proceeding
|
||||||
|
- **AND** only executes if the user explicitly confirms
|
||||||
|
|
||||||
|
#### Scenario: Insufficient permissions for reset
|
||||||
|
|
||||||
|
- **WHEN** a user without proper permissions attempts to reset a device
|
||||||
|
- **THEN** the system returns a 403 forbidden error
|
||||||
|
|
||||||
|
### Requirement: Set Device Speed Limit
|
||||||
|
|
||||||
|
The system SHALL provide the ability to configure upload and download speed limits for an IoT device.
|
||||||
|
|
||||||
|
#### Scenario: Successfully set speed limit
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user submits valid speed limit parameters
|
||||||
|
- **THEN** the system applies the upload and download speed limits to the device
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Configure speed limits via dialog
|
||||||
|
|
||||||
|
- **WHEN** a user selects the speed limit option
|
||||||
|
- **THEN** the system displays a dialog with input fields for upload_speed and download_speed (KB/s)
|
||||||
|
- **AND** validates that both values are integers >= 1
|
||||||
|
|
||||||
|
#### Scenario: Invalid speed limit parameters
|
||||||
|
|
||||||
|
- **WHEN** a user submits speed limits less than 1 KB/s
|
||||||
|
- **THEN** the system returns a 400 parameter error
|
||||||
|
- **AND** displays validation error messages
|
||||||
|
|
||||||
|
### Requirement: Switch Device SIM Card
|
||||||
|
|
||||||
|
The system SHALL provide the ability to switch the active SIM card on a multi-SIM device.
|
||||||
|
|
||||||
|
#### Scenario: Successfully switch to target card
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a card switch with valid target ICCID
|
||||||
|
- **THEN** the system switches the device to use the specified card
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Switch card via dialog
|
||||||
|
|
||||||
|
- **WHEN** a user selects the switch card option
|
||||||
|
- **THEN** the system displays a dialog prompting for target_iccid
|
||||||
|
- **AND** validates the ICCID format
|
||||||
|
|
||||||
|
#### Scenario: Switch to non-existent card
|
||||||
|
|
||||||
|
- **WHEN** a user attempts to switch to an ICCID that doesn't exist or isn't available
|
||||||
|
- **THEN** the system returns a 400 error with descriptive message
|
||||||
|
|
||||||
|
### Requirement: Configure Device WiFi Settings
|
||||||
|
|
||||||
|
The system SHALL provide the ability to configure WiFi settings including SSID, password, and enabled status.
|
||||||
|
|
||||||
|
#### Scenario: Successfully configure WiFi
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user submits valid WiFi configuration
|
||||||
|
- **THEN** the system applies the WiFi settings to the device
|
||||||
|
- **AND** returns a success response
|
||||||
|
|
||||||
|
#### Scenario: Configure WiFi via dialog
|
||||||
|
|
||||||
|
- **WHEN** a user selects the WiFi configuration option
|
||||||
|
- **THEN** the system displays a dialog with fields for:
|
||||||
|
- enabled (启用状态: 0=禁用, 1=启用)
|
||||||
|
- ssid (WiFi名称, 1-32 characters)
|
||||||
|
- password (WiFi密码, 8-63 characters)
|
||||||
|
- **AND** validates all field constraints
|
||||||
|
|
||||||
|
#### Scenario: Invalid WiFi parameters
|
||||||
|
|
||||||
|
- **WHEN** a user submits WiFi configuration with invalid parameters
|
||||||
|
- **THEN** the system returns a 400 error
|
||||||
|
- **AND** displays specific validation errors (e.g., "SSID too long", "Password too short")
|
||||||
|
|
||||||
|
### Requirement: Device Management UI Integration
|
||||||
|
|
||||||
|
The system SHALL integrate device operations into the device management page at `/asset-management/devices`.
|
||||||
|
|
||||||
|
#### Scenario: Display primary operation button
|
||||||
|
|
||||||
|
- **WHEN** a user views the device management page
|
||||||
|
- **THEN** the system displays a "重启设备" (Reboot Device) button as the primary operation
|
||||||
|
|
||||||
|
#### Scenario: Display dropdown menu for additional operations
|
||||||
|
|
||||||
|
- **WHEN** a user views the device management page
|
||||||
|
- **THEN** the system displays a "更多操作" (More Operations) dropdown menu
|
||||||
|
- **AND** the dropdown contains: 恢复出厂, 设置限速, 切换SIM卡, 设置WiFi
|
||||||
|
|
||||||
|
#### Scenario: Show loading indicator during operations
|
||||||
|
|
||||||
|
- **WHEN** a device operation is in progress
|
||||||
|
- **THEN** the system displays a loading indicator
|
||||||
|
- **AND** disables the operation buttons to prevent duplicate requests
|
||||||
|
|
||||||
|
### Requirement: Authentication and Authorization
|
||||||
|
|
||||||
|
The system SHALL enforce JWT-based authentication for all device operations.
|
||||||
|
|
||||||
|
#### Scenario: Access with valid JWT token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with a valid Bearer token
|
||||||
|
- **THEN** the system processes the request normally
|
||||||
|
|
||||||
|
#### Scenario: Access with expired token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with an expired JWT token
|
||||||
|
- **THEN** the system returns a 401 unauthorized error
|
||||||
|
- **AND** redirects to the login page
|
||||||
|
|
||||||
|
### Requirement: Error Handling and User Feedback
|
||||||
|
|
||||||
|
The system SHALL provide clear error messages and success notifications for all operations.
|
||||||
|
|
||||||
|
#### Scenario: Display success message
|
||||||
|
|
||||||
|
- **WHEN** a device operation completes successfully
|
||||||
|
- **THEN** the system displays a success message notification
|
||||||
|
- **AND** automatically closes the operation dialog
|
||||||
|
|
||||||
|
#### Scenario: Handle network errors
|
||||||
|
|
||||||
|
- **WHEN** a network error occurs during a device operation
|
||||||
|
- **THEN** the system displays a user-friendly error message
|
||||||
|
- **AND** allows the user to retry the operation
|
||||||
|
|
||||||
|
#### Scenario: Handle server errors
|
||||||
|
|
||||||
|
- **WHEN** a 500 server error occurs
|
||||||
|
- **THEN** the system displays an error message with timestamp
|
||||||
|
- **AND** logs the error for debugging
|
||||||
|
|
||||||
|
### Requirement: Operation Confirmation Dialogs
|
||||||
|
|
||||||
|
The system SHALL require user confirmation for destructive operations.
|
||||||
|
|
||||||
|
#### Scenario: Confirm reboot operation
|
||||||
|
|
||||||
|
- **WHEN** a user initiates a reboot
|
||||||
|
- **THEN** the system shows a confirmation dialog stating "确定要重启该设备吗?"
|
||||||
|
- **AND** requires explicit confirmation
|
||||||
|
|
||||||
|
#### Scenario: Confirm factory reset operation
|
||||||
|
|
||||||
|
- **WHEN** a user initiates a factory reset
|
||||||
|
- **THEN** the system shows a strong warning dialog stating "确定要恢复出厂设置吗?此操作将清除所有数据!"
|
||||||
|
- **AND** requires explicit confirmation
|
||||||
|
|
||||||
|
#### Scenario: No confirmation for query operations
|
||||||
|
|
||||||
|
- **WHEN** a user initiates speed limit, card switch, or WiFi configuration
|
||||||
|
- **THEN** the system displays input dialogs without confirmation prompts
|
||||||
|
- **AND** only executes after user submits valid parameters
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# IoT Card Operations Specification
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Query IoT Card Flow Usage
|
||||||
|
|
||||||
|
The system SHALL provide the ability to query real-time flow usage for an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully query flow usage
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests flow usage for a valid ICCID
|
||||||
|
- **THEN** the system returns flow usage data including used flow amount and unit
|
||||||
|
|
||||||
|
#### Scenario: Query with invalid ICCID
|
||||||
|
|
||||||
|
- **WHEN** a user requests flow usage for an invalid or non-existent ICCID
|
||||||
|
- **THEN** the system returns a 400 error with appropriate error message
|
||||||
|
|
||||||
|
### Requirement: Query IoT Card Realname Status
|
||||||
|
|
||||||
|
The system SHALL provide the ability to query the realname authentication status for an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully query realname status
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests realname status for a valid ICCID
|
||||||
|
- **THEN** the system returns the current realname authentication status
|
||||||
|
|
||||||
|
#### Scenario: Query status for unauthenticated user
|
||||||
|
|
||||||
|
- **WHEN** an unauthenticated user attempts to query realname status
|
||||||
|
- **THEN** the system returns a 401 unauthorized error
|
||||||
|
|
||||||
|
### Requirement: Query IoT Card Real-time Status
|
||||||
|
|
||||||
|
The system SHALL provide the ability to query the real-time operational status of an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully query card status
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests card status for a valid ICCID
|
||||||
|
- **THEN** the system returns card status information including ICCID and current status (准备/正常/停机)
|
||||||
|
|
||||||
|
#### Scenario: Query with unauthorized access
|
||||||
|
|
||||||
|
- **WHEN** a user without proper permissions attempts to query card status
|
||||||
|
- **THEN** the system returns a 403 forbidden error
|
||||||
|
|
||||||
|
### Requirement: Get IoT Card Realname Link
|
||||||
|
|
||||||
|
The system SHALL provide the ability to generate and retrieve a realname authentication link for an IoT card.
|
||||||
|
|
||||||
|
#### Scenario: Successfully retrieve realname link
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user requests the realname link for a valid ICCID
|
||||||
|
- **THEN** the system returns an HTTPS URL that can be used for realname authentication
|
||||||
|
|
||||||
|
#### Scenario: Display realname link as QR code
|
||||||
|
|
||||||
|
- **WHEN** the system returns a realname authentication link
|
||||||
|
- **THEN** the UI displays the link as a QR code for easy mobile scanning
|
||||||
|
|
||||||
|
### Requirement: Start IoT Card (复机)
|
||||||
|
|
||||||
|
The system SHALL provide the ability to start (restore service) an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully start a stopped card
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a start operation on a stopped card
|
||||||
|
- **THEN** the system processes the request and restores card service
|
||||||
|
- **AND** displays a success message to the user
|
||||||
|
|
||||||
|
#### Scenario: Start operation with confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the start card button
|
||||||
|
- **THEN** the system prompts for confirmation before executing the operation
|
||||||
|
- **AND** only proceeds if the user confirms the action
|
||||||
|
|
||||||
|
#### Scenario: Insufficient permissions for start operation
|
||||||
|
|
||||||
|
- **WHEN** a user without proper permissions attempts to start a card
|
||||||
|
- **THEN** the system returns a 403 forbidden error
|
||||||
|
|
||||||
|
### Requirement: Stop IoT Card (停机)
|
||||||
|
|
||||||
|
The system SHALL provide the ability to stop (suspend service) an IoT card via its ICCID.
|
||||||
|
|
||||||
|
#### Scenario: Successfully stop an active card
|
||||||
|
|
||||||
|
- **WHEN** an authenticated user initiates a stop operation on an active card
|
||||||
|
- **THEN** the system processes the request and suspends card service
|
||||||
|
- **AND** displays a success message to the user
|
||||||
|
|
||||||
|
#### Scenario: Stop operation with confirmation
|
||||||
|
|
||||||
|
- **WHEN** a user clicks the stop card button
|
||||||
|
- **THEN** the system prompts for confirmation before executing the operation
|
||||||
|
- **AND** only proceeds if the user confirms the action
|
||||||
|
|
||||||
|
#### Scenario: Server error during stop operation
|
||||||
|
|
||||||
|
- **WHEN** a server error occurs during the stop operation
|
||||||
|
- **THEN** the system returns a 500 error with error details
|
||||||
|
- **AND** displays an appropriate error message to the user
|
||||||
|
|
||||||
|
### Requirement: IoT Card Management UI Integration
|
||||||
|
|
||||||
|
The system SHALL integrate IoT card operations into the card management page at `/asset-management/iot-card-management`.
|
||||||
|
|
||||||
|
#### Scenario: Display primary operation button
|
||||||
|
|
||||||
|
- **WHEN** a user views the IoT card management page
|
||||||
|
- **THEN** the system displays a "查询流量使用" (Query Flow Usage) button as the primary operation
|
||||||
|
|
||||||
|
#### Scenario: Display dropdown menu for additional operations
|
||||||
|
|
||||||
|
- **WHEN** a user views the IoT card management page
|
||||||
|
- **THEN** the system displays a "更多操作" (More Operations) dropdown menu
|
||||||
|
- **AND** the dropdown contains: 查询实名状态, 查询卡状态, 获取实名链接, 启用卡片, 停用卡片
|
||||||
|
|
||||||
|
#### Scenario: Show operation results in dialog
|
||||||
|
|
||||||
|
- **WHEN** a user executes a query operation (flow, realname status, or card status)
|
||||||
|
- **THEN** the system displays results in a modal dialog with properly formatted data
|
||||||
|
|
||||||
|
### Requirement: Authentication and Authorization
|
||||||
|
|
||||||
|
The system SHALL enforce JWT-based authentication for all IoT card operations.
|
||||||
|
|
||||||
|
#### Scenario: Access with valid JWT token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with a valid Bearer token
|
||||||
|
- **THEN** the system processes the request normally
|
||||||
|
|
||||||
|
#### Scenario: Access with expired token
|
||||||
|
|
||||||
|
- **WHEN** a user makes a request with an expired JWT token
|
||||||
|
- **THEN** the system returns a 401 unauthorized error
|
||||||
|
|
||||||
|
### Requirement: Error Handling
|
||||||
|
|
||||||
|
The system SHALL provide clear error messages for all failure scenarios.
|
||||||
|
|
||||||
|
#### Scenario: Handle 400 parameter errors
|
||||||
|
|
||||||
|
- **WHEN** a request contains invalid parameters
|
||||||
|
- **THEN** the system returns a 400 error with specific validation failure details
|
||||||
|
|
||||||
|
#### Scenario: Handle 500 server errors
|
||||||
|
|
||||||
|
- **WHEN** an internal server error occurs
|
||||||
|
- **THEN** the system returns a 500 error with error timestamp
|
||||||
|
- **AND** logs the error for debugging purposes
|
||||||
86
openspec/changes/add-iot-device-operations/tasks.md
Normal file
86
openspec/changes/add-iot-device-operations/tasks.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Implementation Tasks
|
||||||
|
|
||||||
|
## 1. API Layer - IoT Card Operations
|
||||||
|
|
||||||
|
- [ ] 1.1 创建 `src/api/modules/iotCard.ts`
|
||||||
|
- [ ] 1.2 实现 `getGatewayFlow(iccid)` - 查询流量使用
|
||||||
|
- [ ] 1.3 实现 `getGatewayRealname(iccid)` - 查询实名状态
|
||||||
|
- [ ] 1.4 实现 `getGatewayStatus(iccid)` - 查询卡片状态
|
||||||
|
- [ ] 1.5 实现 `getRealnameLink(iccid)` - 获取实名链接
|
||||||
|
- [ ] 1.6 实现 `startCard(iccid)` - 启用卡片
|
||||||
|
- [ ] 1.7 实现 `stopCard(iccid)` - 停用卡片
|
||||||
|
|
||||||
|
## 2. API Layer - Device Operations
|
||||||
|
|
||||||
|
- [ ] 2.1 创建 `src/api/modules/device.ts`
|
||||||
|
- [ ] 2.2 实现 `rebootDevice(imei)` - 重启设备
|
||||||
|
- [ ] 2.3 实现 `resetDevice(imei)` - 重置设备
|
||||||
|
- [ ] 2.4 实现 `setSpeedLimit(imei, params)` - 设置限速
|
||||||
|
- [ ] 2.5 实现 `switchCard(imei, params)` - 切换SIM卡
|
||||||
|
- [ ] 2.6 实现 `setWifi(imei, params)` - 设置WiFi
|
||||||
|
|
||||||
|
## 3. Type Definitions - IoT Card
|
||||||
|
|
||||||
|
- [ ] 3.1 创建 `src/types/api/iotCard.ts`
|
||||||
|
- [ ] 3.2 定义流量使用响应类型 `GatewayFlowResponse`
|
||||||
|
- [ ] 3.3 定义实名状态响应类型 `GatewayRealnameResponse`
|
||||||
|
- [ ] 3.4 定义卡片状态响应类型 `GatewayStatusResponse`
|
||||||
|
- [ ] 3.5 定义实名链接响应类型 `RealnameUrlResponse`
|
||||||
|
- [ ] 3.6 定义启停操作请求类型 `StartStopCardRequest`
|
||||||
|
|
||||||
|
## 4. Type Definitions - Device
|
||||||
|
|
||||||
|
- [ ] 4.1 创建 `src/types/api/device.ts`
|
||||||
|
- [ ] 4.2 定义限速参数类型 `SpeedLimitParams`
|
||||||
|
- [ ] 4.3 定义换卡参数类型 `SwitchCardParams`
|
||||||
|
- [ ] 4.4 定义WiFi参数类型 `WifiParams`
|
||||||
|
- [ ] 4.5 定义操作响应类型 `DeviceOperationResponse`
|
||||||
|
|
||||||
|
## 5. UI - IoT Card Management Page
|
||||||
|
|
||||||
|
- [ ] 5.1 在表格操作列添加"查询流量"按钮
|
||||||
|
- [ ] 5.2 在表格操作列添加"更多操作"下拉菜单
|
||||||
|
- [ ] 5.3 创建"流量使用查询"对话框组件
|
||||||
|
- [ ] 5.4 创建"实名状态查询"对话框组件
|
||||||
|
- [ ] 5.5 创建"卡片状态查询"对话框组件
|
||||||
|
- [ ] 5.6 创建"获取实名链接"对话框组件(显示二维码)
|
||||||
|
- [ ] 5.7 实现"启用卡片"操作(带确认提示)
|
||||||
|
- [ ] 5.8 实现"停用卡片"操作(带确认提示)
|
||||||
|
|
||||||
|
## 6. UI - Device Management Page
|
||||||
|
|
||||||
|
- [ ] 6.1 在表格操作列添加"重启设备"按钮
|
||||||
|
- [ ] 6.2 在表格操作列添加"更多操作"下拉菜单
|
||||||
|
- [ ] 6.3 实现"重启设备"操作(带确认提示)
|
||||||
|
- [ ] 6.4 实现"重置设备"操作(带确认提示)
|
||||||
|
- [ ] 6.5 创建"设置限速"对话框组件(包含上下行速率输入)
|
||||||
|
- [ ] 6.6 创建"切换SIM卡"对话框组件(选择卡槽)
|
||||||
|
- [ ] 6.7 创建"设置WiFi"对话框组件(SSID、密码、频段等)
|
||||||
|
|
||||||
|
## 7. Error Handling & User Feedback
|
||||||
|
|
||||||
|
- [ ] 7.1 为所有API调用添加错误处理
|
||||||
|
- [ ] 7.2 添加操作成功提示消息
|
||||||
|
- [ ] 7.3 添加操作失败错误提示
|
||||||
|
- [ ] 7.4 添加加载状态指示器
|
||||||
|
|
||||||
|
## 8. Permission Control
|
||||||
|
|
||||||
|
- [ ] 8.1 检查IoT卡操作权限配置
|
||||||
|
- [ ] 8.2 检查设备操作权限配置
|
||||||
|
- [ ] 8.3 根据权限显示/隐藏操作按钮
|
||||||
|
|
||||||
|
## 9. Testing & Validation
|
||||||
|
|
||||||
|
- [ ] 9.1 测试所有IoT卡操作API调用
|
||||||
|
- [ ] 9.2 测试所有设备操作API调用
|
||||||
|
- [ ] 9.3 测试UI交互和对话框显示
|
||||||
|
- [ ] 9.4 测试错误处理场景
|
||||||
|
- [ ] 9.5 测试权限控制
|
||||||
|
- [ ] 9.6 验证响应数据格式和显示
|
||||||
|
|
||||||
|
## 10. Documentation
|
||||||
|
|
||||||
|
- [ ] 10.1 更新API文档(如有需要)
|
||||||
|
- [ ] 10.2 添加操作说明注释
|
||||||
|
- [ ] 10.3 更新用户手册(如有需要)
|
||||||
22
openspec/changes/add-shop-default-roles/proposal.md
Normal file
22
openspec/changes/add-shop-default-roles/proposal.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Change: Add Shop Default Roles Management
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
店铺管理模块需要支持为每个店铺配置默认角色的功能。当新账号加入店铺时,系统需要自动分配这些默认角色,以简化账号管理流程并确保权限一致性。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增查询店铺默认角色列表的接口和UI
|
||||||
|
- 新增为店铺分配默认角色的接口和UI
|
||||||
|
- 新增删除店铺默认角色的接口和UI
|
||||||
|
- 在店铺管理列表页(`/shop-management/list`)添加"设置默认角色"操作入口
|
||||||
|
- 添加店铺默认角色管理对话框组件
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- 影响的规范: shop-management (新增)
|
||||||
|
- 影响的代码:
|
||||||
|
- `src/api/modules/shop.ts` - 新增3个API方法
|
||||||
|
- `src/types/api/shop.ts` - 新增类型定义
|
||||||
|
- `src/views/shop-management/list/index.vue` - 添加默认角色管理UI (假设该页面存在,如不存在需创建)
|
||||||
|
- 依赖的现有功能: 角色管理系统
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Shop Management - Specification Delta
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Query Shop Default Roles
|
||||||
|
|
||||||
|
系统SHALL支持查询指定店铺的默认角色列表。
|
||||||
|
|
||||||
|
**API端点**: `GET /api/admin/shops/{shop_id}/roles`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `shop_id` (integer, required): 店铺ID
|
||||||
|
|
||||||
|
**响应数据**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
roles: ShopRole[] | null // 角色列表
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShopRole {
|
||||||
|
role_id: number // 角色ID
|
||||||
|
role_name: string // 角色名称
|
||||||
|
role_desc: string // 角色描述
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
status: number // 状态 (0:禁用, 1:启用)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario: 成功查询店铺默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户请求获取店铺ID为123的默认角色列表
|
||||||
|
- **THEN** 系统返回该店铺配置的所有默认角色
|
||||||
|
- **AND** 响应包含角色ID、名称、描述、状态等完整信息
|
||||||
|
|
||||||
|
#### Scenario: 查询不存在的店铺
|
||||||
|
|
||||||
|
- **WHEN** 用户请求获取不存在的店铺的默认角色
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明店铺不存在
|
||||||
|
|
||||||
|
#### Scenario: 店铺没有配置默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户请求获取未配置默认角色的店铺
|
||||||
|
- **THEN** 系统返回成功响应
|
||||||
|
- **AND** `roles` 字段为空数组或null
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Assign Shop Default Roles
|
||||||
|
|
||||||
|
系统SHALL支持为指定店铺分配一个或多个默认角色。
|
||||||
|
|
||||||
|
**API端点**: `POST /api/admin/shops/{shop_id}/roles`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `shop_id` (integer, required): 店铺ID
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
role_ids: number[] | null // 角色ID列表
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应数据**: 与"Query Shop Default Roles"相同的数据结构
|
||||||
|
|
||||||
|
#### Scenario: 成功分配单个默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户为店铺分配一个新的默认角色
|
||||||
|
- **THEN** 系统成功保存该角色配置
|
||||||
|
- **AND** 返回更新后的店铺默认角色列表
|
||||||
|
- **AND** 新分配的角色出现在列表中
|
||||||
|
|
||||||
|
#### Scenario: 成功分配多个默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户为店铺分配多个默认角色
|
||||||
|
- **THEN** 系统批量保存所有角色配置
|
||||||
|
- **AND** 返回完整的默认角色列表
|
||||||
|
- **AND** 所有新分配的角色都出现在列表中
|
||||||
|
|
||||||
|
#### Scenario: 分配已存在的角色
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试分配店铺已配置的默认角色
|
||||||
|
- **THEN** 系统应当优雅处理(不重复添加或返回明确提示)
|
||||||
|
- **AND** 不影响其他正常角色的分配
|
||||||
|
|
||||||
|
#### Scenario: 分配不存在的角色
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试分配不存在的角色ID
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明哪些角色ID无效
|
||||||
|
|
||||||
|
#### Scenario: 空角色列表
|
||||||
|
|
||||||
|
- **WHEN** 用户提交空的角色ID列表
|
||||||
|
- **THEN** 系统接受请求
|
||||||
|
- **AND** 可能清空当前默认角色或保持不变(取决于业务需求)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Delete Shop Default Role
|
||||||
|
|
||||||
|
系统SHALL支持删除指定店铺的某个默认角色配置。
|
||||||
|
|
||||||
|
**API端点**: `DELETE /api/admin/shops/{shop_id}/roles/{role_id}`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `shop_id` (integer, required): 店铺ID
|
||||||
|
- `role_id` (integer, required): 角色ID
|
||||||
|
|
||||||
|
**响应**: 标准成功/错误响应
|
||||||
|
|
||||||
|
#### Scenario: 成功删除默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户删除店铺的一个默认角色配置
|
||||||
|
- **THEN** 系统成功移除该角色配置
|
||||||
|
- **AND** 返回成功响应(code: 0)
|
||||||
|
- **AND** 后续查询该店铺默认角色列表时不再包含该角色
|
||||||
|
|
||||||
|
#### Scenario: 删除不存在的角色配置
|
||||||
|
|
||||||
|
- **WHEN** 用户尝试删除店铺未配置的角色
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明该角色不在店铺的默认角色列表中
|
||||||
|
|
||||||
|
#### Scenario: 删除不存在的店铺或角色
|
||||||
|
|
||||||
|
- **WHEN** 用户使用无效的shop_id或role_id
|
||||||
|
- **THEN** 系统返回400错误
|
||||||
|
- **AND** 错误消息说明参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Shop Default Roles UI
|
||||||
|
|
||||||
|
店铺管理页面SHALL提供默认角色管理的用户界面。
|
||||||
|
|
||||||
|
#### Scenario: 在店铺列表中访问默认角色设置
|
||||||
|
|
||||||
|
- **WHEN** 用户在店铺管理列表页查看店铺列表
|
||||||
|
- **THEN** 每个店铺的操作列应当包含"设置默认角色"按钮或菜单项
|
||||||
|
- **AND** 点击后打开默认角色管理对话框
|
||||||
|
|
||||||
|
#### Scenario: 查看店铺当前默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户打开某店铺的默认角色管理对话框
|
||||||
|
- **THEN** 对话框显示该店铺当前已配置的所有默认角色
|
||||||
|
- **AND** 每个角色显示名称、描述和状态
|
||||||
|
- **AND** 提供删除按钮用于移除默认角色
|
||||||
|
|
||||||
|
#### Scenario: 添加默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户在对话框中选择要添加的角色
|
||||||
|
- **THEN** 系统提供角色选择器,展示可用的系统角色列表
|
||||||
|
- **AND** 支持多选
|
||||||
|
- **AND** 点击确认后调用分配接口
|
||||||
|
- **AND** 成功后刷新默认角色列表
|
||||||
|
|
||||||
|
#### Scenario: 删除默认角色
|
||||||
|
|
||||||
|
- **WHEN** 用户点击某个默认角色的删除按钮
|
||||||
|
- **THEN** 系统显示确认对话框
|
||||||
|
- **AND** 用户确认后调用删除接口
|
||||||
|
- **AND** 成功后从列表中移除该角色
|
||||||
|
|
||||||
|
#### Scenario: 加载状态和错误处理
|
||||||
|
|
||||||
|
- **WHEN** 进行任何API操作时
|
||||||
|
- **THEN** 界面应当显示加载状态(loading indicator)
|
||||||
|
- **AND** 如果操作失败,应当显示友好的错误消息
|
||||||
|
- **AND** 允许用户重试或关闭对话框
|
||||||
62
openspec/changes/add-shop-default-roles/tasks.md
Normal file
62
openspec/changes/add-shop-default-roles/tasks.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Implementation Tasks
|
||||||
|
|
||||||
|
## 1. 类型定义
|
||||||
|
|
||||||
|
- [x] 1.1 在 `src/types/api/shop.ts` 中添加 `ShopRoleResponse` 接口
|
||||||
|
- [x] 1.2 在 `src/types/api/shop.ts` 中添加 `ShopRolesResponse` 接口
|
||||||
|
- [x] 1.3 在 `src/types/api/shop.ts` 中添加 `AssignShopRolesRequest` 接口
|
||||||
|
|
||||||
|
## 2. API 服务层
|
||||||
|
|
||||||
|
- [x] 2.1 在 `ShopService` 中添加 `getShopRoles(shopId)` 方法
|
||||||
|
- [x] 2.2 在 `ShopService` 中添加 `assignShopRoles(shopId, roleIds)` 方法
|
||||||
|
- [x] 2.3 在 `ShopService` 中添加 `deleteShopRole(shopId, roleId)` 方法
|
||||||
|
|
||||||
|
## 3. UI 组件开发
|
||||||
|
|
||||||
|
- [x] 3.1 检查店铺管理列表页面 (`src/views/product/shop/index.vue` 已存在)
|
||||||
|
- [x] 3.2 在店铺列表操作列中添加"默认角色"按钮
|
||||||
|
- [x] 3.3 创建店铺默认角色管理对话框组件
|
||||||
|
- [x] 3.4 实现查询店铺默认角色列表功能
|
||||||
|
- [x] 3.5 实现添加默认角色功能(支持多选角色)
|
||||||
|
- [x] 3.6 实现删除默认角色功能
|
||||||
|
- [x] 3.7 添加角色选择器(从系统角色列表中选择)
|
||||||
|
- [x] 3.8 添加加载状态和错误处理
|
||||||
|
|
||||||
|
## 4. 验证和测试
|
||||||
|
|
||||||
|
- [ ] 4.1 测试查询店铺默认角色列表
|
||||||
|
- [ ] 4.2 测试添加默认角色(单个和多个)
|
||||||
|
- [ ] 4.3 测试删除默认角色
|
||||||
|
- [ ] 4.4 测试边界情况(角色已存在、店铺不存在等)
|
||||||
|
- [ ] 4.5 测试权限控制(只有有权限的用户才能操作)
|
||||||
|
|
||||||
|
## 实现说明
|
||||||
|
|
||||||
|
### 完成的功能
|
||||||
|
|
||||||
|
1. **类型定义** - 已在 `src/types/api/shop.ts` 添加了三个接口:
|
||||||
|
- `ShopRoleResponse`: 店铺角色响应实体
|
||||||
|
- `ShopRolesResponse`: 店铺角色列表响应
|
||||||
|
- `AssignShopRolesRequest`: 分配角色请求
|
||||||
|
|
||||||
|
2. **API 服务层** - 已在 `src/api/modules/shop.ts` 的 `ShopService` 类中添加:
|
||||||
|
- `getShopRoles(shopId)`: 获取店铺默认角色列表
|
||||||
|
- `assignShopRoles(shopId, data)`: 分配店铺默认角色
|
||||||
|
- `deleteShopRole(shopId, roleId)`: 删除店铺默认角色
|
||||||
|
|
||||||
|
3. **UI 实现** - 已在 `src/views/product/shop/index.vue` 中实现:
|
||||||
|
- 操作列新增"默认角色"按钮 (宽度从220改为280)
|
||||||
|
- 默认角色管理对话框,显示当前默认角色列表,支持删除操作
|
||||||
|
- 添加角色对话框,支持多选角色,已分配的角色显示为禁用状态
|
||||||
|
- 完整的加载状态(loading indicators)
|
||||||
|
- 错误处理和友好的提示消息
|
||||||
|
- 使用 `ShopService` 和 `RoleService` 调用后端API
|
||||||
|
|
||||||
|
### 待测试项
|
||||||
|
|
||||||
|
所有开发任务已完成,现在需要进行功能测试以确保:
|
||||||
|
- API 调用正常工作
|
||||||
|
- UI 交互符合预期
|
||||||
|
- 边界情况处理正确
|
||||||
|
- 权限控制生效
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"pinia-plugin-persistedstate": "^4.3.0",
|
"pinia-plugin-persistedstate": "^4.3.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
|
|||||||
6817
pnpm-lock.yaml
generated
6817
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,11 @@ import type {
|
|||||||
CardOrder,
|
CardOrder,
|
||||||
BaseResponse,
|
BaseResponse,
|
||||||
PaginationResponse,
|
PaginationResponse,
|
||||||
ListResponse
|
ListResponse,
|
||||||
|
GatewayFlowUsageResponse,
|
||||||
|
GatewayRealnameStatusResponse,
|
||||||
|
GatewayCardStatusResponse,
|
||||||
|
GatewayRealnameLinkResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class CardService extends BaseService {
|
export class CardService extends BaseService {
|
||||||
@@ -367,4 +371,62 @@ export class CardService extends BaseService {
|
|||||||
}): Promise<BaseResponse<any>> {
|
}): Promise<BaseResponse<any>> {
|
||||||
return this.patch('/api/admin/iot-cards/series-binding', data)
|
return this.patch('/api/admin/iot-cards/series-binding', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== IoT卡网关操作相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询流量使用
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static getGatewayFlow(iccid: string): Promise<BaseResponse<GatewayFlowUsageResponse>> {
|
||||||
|
return this.get<BaseResponse<GatewayFlowUsageResponse>>(
|
||||||
|
`/api/admin/iot-cards/${iccid}/gateway-flow`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询实名认证状态
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static getGatewayRealname(iccid: string): Promise<BaseResponse<GatewayRealnameStatusResponse>> {
|
||||||
|
return this.get<BaseResponse<GatewayRealnameStatusResponse>>(
|
||||||
|
`/api/admin/iot-cards/${iccid}/gateway-realname`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询卡实时状态
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static getGatewayStatus(iccid: string): Promise<BaseResponse<GatewayCardStatusResponse>> {
|
||||||
|
return this.get<BaseResponse<GatewayCardStatusResponse>>(
|
||||||
|
`/api/admin/iot-cards/${iccid}/gateway-status`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实名认证链接
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static getRealnameLink(iccid: string): Promise<BaseResponse<GatewayRealnameLinkResponse>> {
|
||||||
|
return this.get<BaseResponse<GatewayRealnameLinkResponse>>(
|
||||||
|
`/api/admin/iot-cards/${iccid}/realname-link`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用物联网卡(复机)
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static startCard(iccid: string): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/iot-cards/${iccid}/start`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停用物联网卡(停机)
|
||||||
|
* @param iccid ICCID
|
||||||
|
*/
|
||||||
|
static stopCard(iccid: string): Promise<BaseResponse> {
|
||||||
|
return this.post<BaseResponse>(`/api/admin/iot-cards/${iccid}/stop`, {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ import type {
|
|||||||
DeviceImportTaskQueryParams,
|
DeviceImportTaskQueryParams,
|
||||||
DeviceImportTaskListResponse,
|
DeviceImportTaskListResponse,
|
||||||
DeviceImportTaskDetail,
|
DeviceImportTaskDetail,
|
||||||
BaseResponse
|
BaseResponse,
|
||||||
|
SetSpeedLimitRequest,
|
||||||
|
SwitchCardRequest,
|
||||||
|
SetWiFiRequest,
|
||||||
|
DeviceOperationResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
export class DeviceService extends BaseService {
|
export class DeviceService extends BaseService {
|
||||||
@@ -157,4 +161,73 @@ export class DeviceService extends BaseService {
|
|||||||
}): Promise<BaseResponse<any>> {
|
}): Promise<BaseResponse<any>> {
|
||||||
return this.patch('/api/admin/devices/series-binding', data)
|
return this.patch('/api/admin/devices/series-binding', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 设备操作相关 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启设备
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
*/
|
||||||
|
static rebootDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/reboot`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复出厂设置
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
*/
|
||||||
|
static resetDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/reset`,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置限速
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
* @param data 限速参数
|
||||||
|
*/
|
||||||
|
static setSpeedLimit(
|
||||||
|
imei: string,
|
||||||
|
data: SetSpeedLimitRequest
|
||||||
|
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/speed-limit`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换SIM卡
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
* @param data 切卡参数
|
||||||
|
*/
|
||||||
|
static switchCard(
|
||||||
|
imei: string,
|
||||||
|
data: SwitchCardRequest
|
||||||
|
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/switch-card`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置WiFi
|
||||||
|
* @param imei 设备号(IMEI)
|
||||||
|
* @param data WiFi参数
|
||||||
|
*/
|
||||||
|
static setWiFi(
|
||||||
|
imei: string,
|
||||||
|
data: SetWiFiRequest
|
||||||
|
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||||
|
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||||
|
`/api/admin/devices/by-imei/${imei}/wifi`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
ShopQueryParams,
|
ShopQueryParams,
|
||||||
CreateShopParams,
|
CreateShopParams,
|
||||||
UpdateShopParams,
|
UpdateShopParams,
|
||||||
|
ShopRolesResponse,
|
||||||
|
AssignShopRolesRequest,
|
||||||
BaseResponse,
|
BaseResponse,
|
||||||
PaginationResponse
|
PaginationResponse
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
@@ -49,4 +51,38 @@ export class ShopService extends BaseService {
|
|||||||
static deleteShop(id: number): Promise<BaseResponse> {
|
static deleteShop(id: number): Promise<BaseResponse> {
|
||||||
return this.delete<BaseResponse>(`/api/admin/shops/${id}`)
|
return this.delete<BaseResponse>(`/api/admin/shops/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 店铺默认角色管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取店铺默认角色列表
|
||||||
|
* GET /api/admin/shops/{shop_id}/roles
|
||||||
|
* @param shopId 店铺ID
|
||||||
|
*/
|
||||||
|
static getShopRoles(shopId: number): Promise<BaseResponse<ShopRolesResponse>> {
|
||||||
|
return this.get<BaseResponse<ShopRolesResponse>>(`/api/admin/shops/${shopId}/roles`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配店铺默认角色
|
||||||
|
* POST /api/admin/shops/{shop_id}/roles
|
||||||
|
* @param shopId 店铺ID
|
||||||
|
* @param data 角色ID列表
|
||||||
|
*/
|
||||||
|
static assignShopRoles(
|
||||||
|
shopId: number,
|
||||||
|
data: AssignShopRolesRequest
|
||||||
|
): Promise<BaseResponse<ShopRolesResponse>> {
|
||||||
|
return this.post<BaseResponse<ShopRolesResponse>>(`/api/admin/shops/${shopId}/roles`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除店铺默认角色
|
||||||
|
* DELETE /api/admin/shops/{shop_id}/roles/{role_id}
|
||||||
|
* @param shopId 店铺ID
|
||||||
|
* @param roleId 角色ID
|
||||||
|
*/
|
||||||
|
static deleteShopRole(shopId: number, roleId: number): Promise<BaseResponse> {
|
||||||
|
return this.delete<BaseResponse>(`/api/admin/shops/${shopId}/roles/${roleId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export interface PlatformAccount {
|
|||||||
phone: string
|
phone: string
|
||||||
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
|
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
|
||||||
enterprise_id?: number | null // 关联企业ID
|
enterprise_id?: number | null // 关联企业ID
|
||||||
|
enterprise_name?: string // ⭐ 新增:企业名称
|
||||||
shop_id?: number | null // 关联店铺ID
|
shop_id?: number | null // 关联店铺ID
|
||||||
|
shop_name?: string // ⭐ 新增:店铺名称
|
||||||
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
status: AccountStatus // 状态 (0:禁用, 1:启用)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +99,12 @@ export interface CustomerAccount {
|
|||||||
|
|
||||||
// 账号查询参数
|
// 账号查询参数
|
||||||
export interface AccountQueryParams extends PaginationParams {
|
export interface AccountQueryParams extends PaginationParams {
|
||||||
keyword?: string // 关键词(用户名、姓名、手机号)
|
username?: string // 用户名模糊查询
|
||||||
roleId?: string | number
|
phone?: string // 手机号模糊查询
|
||||||
status?: AccountStatus
|
user_type?: number // 用户类型 (1-4)
|
||||||
createTimeRange?: [string, string]
|
status?: AccountStatus // 状态 (0:禁用, 1:启用)
|
||||||
|
shop_id?: number // 按店铺ID筛选
|
||||||
|
enterprise_id?: number // 按企业ID筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代理商查询参数
|
// 代理商查询参数
|
||||||
|
|||||||
@@ -505,3 +505,31 @@ export interface BatchSetCardSeriesBindingResponse {
|
|||||||
fail_count: number // 失败数量
|
fail_count: number // 失败数量
|
||||||
failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表
|
failed_items: CardSeriesBindingFailedItem[] | null // 失败详情列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== IoT卡网关操作相关 ==========
|
||||||
|
|
||||||
|
// 查询流量使用响应
|
||||||
|
export interface GatewayFlowUsageResponse {
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
unit?: string // 流量单位(MB)
|
||||||
|
usedFlow?: number // 已用流量
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询实名认证状态响应
|
||||||
|
export interface GatewayRealnameStatusResponse {
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
status?: string // 实名认证状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询卡实时状态响应
|
||||||
|
export interface GatewayCardStatusResponse {
|
||||||
|
cardStatus?: string // 卡状态(准备、正常、停机)
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
iccid?: string // ICCID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实名认证链接响应
|
||||||
|
export interface GatewayRealnameLinkResponse {
|
||||||
|
extend?: string // 扩展字段(广电国网特殊参数)
|
||||||
|
link?: string // 实名认证跳转链接(HTTPS URL)
|
||||||
|
}
|
||||||
|
|||||||
@@ -225,3 +225,28 @@ export interface BatchSetDeviceSeriesBindingResponse {
|
|||||||
fail_count: number // 失败数量
|
fail_count: number // 失败数量
|
||||||
failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表
|
failed_items: DeviceSeriesBindingFailedItem[] | null // 失败详情列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 设备操作相关 ==========
|
||||||
|
|
||||||
|
// 设置限速请求参数
|
||||||
|
export interface SetSpeedLimitRequest {
|
||||||
|
download_speed: number // 下行速率(KB/s)
|
||||||
|
upload_speed: number // 上行速率(KB/s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换SIM卡请求参数
|
||||||
|
export interface SwitchCardRequest {
|
||||||
|
target_iccid: string // 目标卡 ICCID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置WiFi请求参数
|
||||||
|
export interface SetWiFiRequest {
|
||||||
|
enabled: number // 启用状态(0:禁用, 1:启用)
|
||||||
|
ssid: string // WiFi 名称(1-32字符)
|
||||||
|
password: string // WiFi 密码(8-63字符)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空响应(设备操作成功)
|
||||||
|
export interface DeviceOperationResponse {
|
||||||
|
message?: string // 提示信息
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,3 +68,26 @@ export interface ShopPageResult {
|
|||||||
size?: number // 每页数量
|
size?: number // 每页数量
|
||||||
total?: number // 总记录数
|
total?: number // 总记录数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 店铺默认角色相关 ==========
|
||||||
|
|
||||||
|
// 店铺角色响应
|
||||||
|
export interface ShopRoleResponse {
|
||||||
|
role_id: number // 角色ID
|
||||||
|
role_name: string // 角色名称
|
||||||
|
role_desc: string // 角色描述
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
status: number // 状态 (0:禁用, 1:启用)
|
||||||
|
role_type?: number // 角色类型 (1:平台角色, 2:客户角色) - 可选,用于UI显示
|
||||||
|
}
|
||||||
|
|
||||||
|
// 店铺角色列表响应
|
||||||
|
export interface ShopRolesResponse {
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
roles: ShopRoleResponse[] | null // 角色列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配店铺角色请求
|
||||||
|
export interface AssignShopRolesRequest {
|
||||||
|
role_ids: number[] | null // 角色ID列表
|
||||||
|
}
|
||||||
|
|||||||
7
src/types/auto-imports.d.ts
vendored
7
src/types/auto-imports.d.ts
vendored
@@ -7,7 +7,12 @@
|
|||||||
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 ElButton2: typeof import('element-plus/es')['ElButton2']
|
||||||
|
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
const ElDropdown2: typeof import('element-plus/es')['ElDropdown2']
|
||||||
|
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
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']
|
||||||
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
||||||
|
|||||||
47
src/utils/codeGenerator.ts
Normal file
47
src/utils/codeGenerator.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 编码生成工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成套餐系列编码
|
||||||
|
* 规则: PS + 年月日时分秒 + 4位随机数
|
||||||
|
* 示例: PS20260203143025ABCD
|
||||||
|
*/
|
||||||
|
export function generateSeriesCode(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
// 生成4位随机大写字母
|
||||||
|
const randomChars = Array.from({ length: 4 }, () =>
|
||||||
|
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return `PS${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成套餐编码
|
||||||
|
* 规则: PKG + 年月日时分秒 + 4位随机数
|
||||||
|
* 示例: PKG20260203143025ABCD
|
||||||
|
*/
|
||||||
|
export function generatePackageCode(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
// 生成4位随机大写字母
|
||||||
|
const randomChars = Array.from({ length: 4 }, () =>
|
||||||
|
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return `PKG${year}${month}${day}${hours}${minutes}${seconds}${randomChars}`
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
v-model:filter="formFilters"
|
v-model:filter="formFilters"
|
||||||
:items="formItems"
|
:items="formItems"
|
||||||
:show-expand="false"
|
|
||||||
@reset="handleReset"
|
@reset="handleReset"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
></ArtSearchBar>
|
></ArtSearchBar>
|
||||||
@@ -121,10 +120,11 @@
|
|||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { AccountService } from '@/api/modules/account'
|
import { AccountService } from '@/api/modules/account'
|
||||||
import { RoleService } from '@/api/modules/role'
|
import { RoleService } from '@/api/modules/role'
|
||||||
|
import { ShopService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type { PlatformRole } from '@/types/api'
|
import type { PlatformRole } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||||
|
|
||||||
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||||
|
|
||||||
@@ -140,12 +140,20 @@
|
|||||||
// 定义表单搜索初始值
|
// 定义表单搜索初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
name: '',
|
name: '',
|
||||||
phone: ''
|
phone: '',
|
||||||
|
user_type: undefined as number | undefined,
|
||||||
|
shop_id: undefined as number | undefined,
|
||||||
|
enterprise_id: undefined as number | undefined,
|
||||||
|
status: undefined as number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式表单数据
|
// 响应式表单数据
|
||||||
const formFilters = reactive({ ...initialSearchState })
|
const formFilters = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 店铺和企业列表
|
||||||
|
const shopList = ref<any[]>([])
|
||||||
|
const enterpriseList = ref<any[]>([])
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -176,7 +184,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 表单配置项
|
// 表单配置项
|
||||||
const formItems: SearchFormItem[] = [
|
const formItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: '账号名称',
|
label: '账号名称',
|
||||||
prop: 'name',
|
prop: 'name',
|
||||||
@@ -194,17 +202,59 @@
|
|||||||
clearable: true,
|
clearable: true,
|
||||||
placeholder: '请输入手机号'
|
placeholder: '请输入手机号'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '账号类型',
|
||||||
|
prop: 'user_type',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '超级管理员', value: 1 },
|
||||||
|
{ label: '平台用户', value: 2 },
|
||||||
|
{ label: '代理账号', value: 3 },
|
||||||
|
{ label: '企业账号', value: 4 }
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择账号类型'
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
label: '关联店铺',
|
||||||
|
prop: 'shop_id',
|
||||||
|
type: 'select',
|
||||||
|
options: shopList.value.map((shop) => ({
|
||||||
|
label: shop.shop_name,
|
||||||
|
value: shop.id
|
||||||
|
})),
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
remote: true,
|
||||||
|
remoteMethod: handleShopSearch,
|
||||||
|
placeholder: '请输入店铺名称搜索'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
prop: 'status',
|
||||||
|
type: 'select',
|
||||||
|
options: STATUS_SELECT_OPTIONS,
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择状态'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ID', prop: 'ID' },
|
|
||||||
{ label: '账号名称', prop: 'username' },
|
{ label: '账号名称', prop: 'username' },
|
||||||
{ label: '手机号', prop: 'phone' },
|
{ label: '手机号', prop: 'phone' },
|
||||||
{ label: '账号类型', prop: 'user_type_name' },
|
{ label: '账号类型', prop: 'user_type' },
|
||||||
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '创建时间', prop: 'CreatedAt' },
|
{ label: '创建时间', prop: 'created_at' },
|
||||||
{ label: '操作', prop: 'operation' }
|
{ label: '操作', prop: 'operation' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -256,21 +306,20 @@
|
|||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
|
||||||
prop: 'ID',
|
|
||||||
label: 'ID'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
label: '账号名称'
|
label: '账号名称',
|
||||||
|
minWidth: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
label: '手机号'
|
label: '手机号',
|
||||||
|
width: 130
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'user_type',
|
prop: 'user_type',
|
||||||
label: '账号类型',
|
label: '账号类型',
|
||||||
|
width: 120,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '超级管理员',
|
1: '超级管理员',
|
||||||
@@ -281,9 +330,26 @@
|
|||||||
return typeMap[row.user_type] || '-'
|
return typeMap[row.user_type] || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'shop_name',
|
||||||
|
label: '店铺名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return row.shop_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'enterprise_name',
|
||||||
|
label: '企业名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return row.enterprise_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
|
width: 100,
|
||||||
formatter: (row: any) => {
|
formatter: (row: any) => {
|
||||||
return h(ElSwitch, {
|
return h(ElSwitch, {
|
||||||
modelValue: row.status,
|
modelValue: row.status,
|
||||||
@@ -298,13 +364,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'CreatedAt',
|
prop: 'created_at',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
formatter: (row: any) => formatDateTime(row.CreatedAt)
|
width: 180,
|
||||||
|
formatter: (row: any) => formatDateTime(row.created_at)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
|
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;' }, [
|
||||||
@@ -340,6 +408,7 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getAccountList()
|
getAccountList()
|
||||||
loadAllRoles()
|
loadAllRoles()
|
||||||
|
loadShopList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载所有角色列表
|
// 加载所有角色列表
|
||||||
@@ -400,7 +469,12 @@
|
|||||||
const params = {
|
const params = {
|
||||||
page: pagination.currentPage,
|
page: pagination.currentPage,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
keyword: formFilters.name || formFilters.phone || undefined
|
username: formFilters.name || undefined,
|
||||||
|
phone: formFilters.phone || undefined,
|
||||||
|
user_type: formFilters.user_type,
|
||||||
|
shop_id: formFilters.shop_id,
|
||||||
|
enterprise_id: formFilters.enterprise_id,
|
||||||
|
status: formFilters.status
|
||||||
}
|
}
|
||||||
const res = await AccountService.getAccounts(params)
|
const res = await AccountService.getAccounts(params)
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -489,7 +563,7 @@
|
|||||||
// 先更新UI
|
// 先更新UI
|
||||||
row.status = newStatus
|
row.status = newStatus
|
||||||
try {
|
try {
|
||||||
await AccountService.updateAccount(row.ID, { status: newStatus })
|
await AccountService.updateAccountStatus(row.ID, newStatus as 0 | 1)
|
||||||
ElMessage.success('状态切换成功')
|
ElMessage.success('状态切换成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 切换失败,恢复原状态
|
// 切换失败,恢复原状态
|
||||||
@@ -497,6 +571,28 @@
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载店铺列表
|
||||||
|
const loadShopList = async (keyword: string = '') => {
|
||||||
|
try {
|
||||||
|
const res = await ShopService.getShops({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20, // 默认加载20条
|
||||||
|
status: 1, // 只加载启用的店铺
|
||||||
|
shop_name: keyword || undefined // 根据店铺名称搜索
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
shopList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取店铺列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 店铺搜索处理
|
||||||
|
const handleShopSearch = (query: string) => {
|
||||||
|
loadShopList(query)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
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 { AccountService, RoleService } from '@/api/modules'
|
import { AccountService, RoleService, ShopService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type { PlatformRole, PlatformAccount } from '@/types/api'
|
import type { PlatformRole, PlatformAccount } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
@@ -189,12 +189,19 @@
|
|||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
username: '',
|
username: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
user_type: undefined as number | undefined,
|
||||||
|
shop_id: undefined as number | undefined,
|
||||||
|
enterprise_id: undefined as number | undefined,
|
||||||
status: undefined as number | undefined
|
status: undefined as number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式表单数据
|
// 响应式表单数据
|
||||||
const searchForm = reactive({ ...initialSearchState })
|
const searchForm = reactive({ ...initialSearchState })
|
||||||
|
|
||||||
|
// 店铺和企业列表
|
||||||
|
const shopList = ref<any[]>([])
|
||||||
|
const enterpriseList = ref<any[]>([])
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -225,7 +232,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 表单配置项
|
// 表单配置项
|
||||||
const searchFormItems: SearchFormItem[] = [
|
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: '账号名称',
|
label: '账号名称',
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
@@ -244,6 +251,35 @@
|
|||||||
placeholder: '请输入手机号'
|
placeholder: '请输入手机号'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '账号类型',
|
||||||
|
prop: 'user_type',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '超级管理员', value: 1 },
|
||||||
|
{ label: '平台用户', value: 2 },
|
||||||
|
{ label: '代理账号', value: 3 },
|
||||||
|
{ label: '企业账号', value: 4 }
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择账号类型'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关联店铺',
|
||||||
|
prop: 'shop_id',
|
||||||
|
type: 'select',
|
||||||
|
options: shopList.value.map((shop) => ({
|
||||||
|
label: shop.shop_name,
|
||||||
|
value: shop.id
|
||||||
|
})),
|
||||||
|
config: {
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
placeholder: '请选择店铺'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '状态',
|
label: '状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -254,7 +290,7 @@
|
|||||||
placeholder: '请选择状态'
|
placeholder: '请选择状态'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
])
|
||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
@@ -262,6 +298,8 @@
|
|||||||
{ label: '账号名称', prop: 'username' },
|
{ label: '账号名称', prop: 'username' },
|
||||||
{ label: '手机号', prop: 'phone' },
|
{ label: '手机号', prop: 'phone' },
|
||||||
{ label: '账号类型', prop: 'user_type' },
|
{ label: '账号类型', prop: 'user_type' },
|
||||||
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
|
{ label: '企业名称', prop: 'enterprise_name' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
{ label: '创建时间', prop: 'CreatedAt' },
|
{ label: '创建时间', prop: 'CreatedAt' },
|
||||||
{ label: '操作', prop: 'operation' }
|
{ label: '操作', prop: 'operation' }
|
||||||
@@ -284,7 +322,7 @@
|
|||||||
formData.user_type = row.user_type
|
formData.user_type = row.user_type
|
||||||
formData.enterprise_id = row.enterprise_id || null
|
formData.enterprise_id = row.enterprise_id || null
|
||||||
formData.shop_id = row.shop_id || null
|
formData.shop_id = row.shop_id || null
|
||||||
formData.status = row.status as CommonStatus
|
formData.status = row.status as unknown as CommonStatus
|
||||||
formData.password = ''
|
formData.password = ''
|
||||||
} else {
|
} else {
|
||||||
formData.id = 0
|
formData.id = 0
|
||||||
@@ -333,19 +371,23 @@
|
|||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
{
|
||||||
prop: 'ID',
|
prop: 'ID',
|
||||||
label: 'ID'
|
label: 'ID',
|
||||||
|
width: 80
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'username',
|
prop: 'username',
|
||||||
label: '账号名称'
|
label: '账号名称',
|
||||||
|
minWidth: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'phone',
|
prop: 'phone',
|
||||||
label: '手机号'
|
label: '手机号',
|
||||||
|
width: 130
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'user_type',
|
prop: 'user_type',
|
||||||
label: '账号类型',
|
label: '账号类型',
|
||||||
|
width: 120,
|
||||||
formatter: (row: PlatformAccount) => {
|
formatter: (row: PlatformAccount) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '超级管理员',
|
1: '超级管理员',
|
||||||
@@ -356,9 +398,26 @@
|
|||||||
return typeMap[row.user_type] || '-'
|
return typeMap[row.user_type] || '-'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'shop_name',
|
||||||
|
label: '店铺名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
|
return row.shop_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'enterprise_name',
|
||||||
|
label: '企业名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: PlatformAccount) => {
|
||||||
|
return row.enterprise_name || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
|
width: 100,
|
||||||
formatter: (row: PlatformAccount) => {
|
formatter: (row: PlatformAccount) => {
|
||||||
return h(ElSwitch, {
|
return h(ElSwitch, {
|
||||||
modelValue: row.status,
|
modelValue: row.status,
|
||||||
@@ -375,6 +434,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'CreatedAt',
|
prop: 'CreatedAt',
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
|
width: 180,
|
||||||
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
|
formatter: (row: PlatformAccount) => formatDateTime(row.CreatedAt)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -429,6 +489,7 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getAccountList()
|
getAccountList()
|
||||||
loadAllRoles()
|
loadAllRoles()
|
||||||
|
loadShopList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载所有角色列表
|
// 加载所有角色列表
|
||||||
@@ -512,9 +573,11 @@
|
|||||||
const params = {
|
const params = {
|
||||||
page: pagination.currentPage,
|
page: pagination.currentPage,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
user_type: 2, // 筛选平台账号
|
|
||||||
username: searchForm.username || undefined,
|
username: searchForm.username || undefined,
|
||||||
phone: searchForm.phone || undefined,
|
phone: searchForm.phone || undefined,
|
||||||
|
user_type: searchForm.user_type, // 账号类型筛选(可选)
|
||||||
|
shop_id: searchForm.shop_id, // 店铺筛选
|
||||||
|
enterprise_id: searchForm.enterprise_id, // 企业筛选
|
||||||
status: searchForm.status
|
status: searchForm.status
|
||||||
}
|
}
|
||||||
const res = await AccountService.getAccounts(params)
|
const res = await AccountService.getAccounts(params)
|
||||||
@@ -659,6 +722,22 @@
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载店铺列表
|
||||||
|
const loadShopList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await ShopService.getShops({
|
||||||
|
page: 1,
|
||||||
|
page_size: 9999,
|
||||||
|
status: 1 // 只加载启用的店铺
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
shopList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取店铺列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -353,6 +353,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 设备操作右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="deviceOperationMenuRef"
|
||||||
|
:menu-items="deviceOperationMenuItems"
|
||||||
|
:menu-width="140"
|
||||||
|
@select="handleDeviceOperationMenuSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 绑定卡弹窗 -->
|
<!-- 绑定卡弹窗 -->
|
||||||
<ElDialog v-model="bindCardDialogVisible" title="绑定卡到设备" width="500px">
|
<ElDialog v-model="bindCardDialogVisible" title="绑定卡到设备" width="500px">
|
||||||
<ElForm ref="bindCardFormRef" :model="bindCardForm" :rules="bindCardRules" label-width="100px">
|
<ElForm ref="bindCardFormRef" :model="bindCardForm" :rules="bindCardRules" label-width="100px">
|
||||||
@@ -394,6 +402,119 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 设置限速对话框 -->
|
||||||
|
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="500px">
|
||||||
|
<ElForm
|
||||||
|
ref="speedLimitFormRef"
|
||||||
|
:model="speedLimitForm"
|
||||||
|
:rules="speedLimitRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="设备号">
|
||||||
|
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="下行速率" prop="download_speed">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="speedLimitForm.download_speed"
|
||||||
|
:min="1"
|
||||||
|
:step="128"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="上行速率" prop="upload_speed">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="speedLimitForm.upload_speed"
|
||||||
|
:min="1"
|
||||||
|
:step="128"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="speedLimitDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleConfirmSpeedLimit" :loading="speedLimitLoading">
|
||||||
|
确认设置
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 切换SIM卡对话框 -->
|
||||||
|
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="500px">
|
||||||
|
<ElForm
|
||||||
|
ref="switchCardFormRef"
|
||||||
|
:model="switchCardForm"
|
||||||
|
:rules="switchCardRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="设备号">
|
||||||
|
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="目标ICCID" prop="target_iccid">
|
||||||
|
<ElInput
|
||||||
|
v-model="switchCardForm.target_iccid"
|
||||||
|
placeholder="请输入要切换到的目标ICCID"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="switchCardDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleConfirmSwitchCard" :loading="switchCardLoading">
|
||||||
|
确认切换
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 设置WiFi对话框 -->
|
||||||
|
<ElDialog v-model="setWiFiDialogVisible" title="设置WiFi" width="500px">
|
||||||
|
<ElForm
|
||||||
|
ref="setWiFiFormRef"
|
||||||
|
:model="setWiFiForm"
|
||||||
|
:rules="setWiFiRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<ElFormItem label="设备号">
|
||||||
|
<span style="font-weight: bold; color: #409eff">{{ currentOperatingDevice }}</span>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="WiFi状态" prop="enabled">
|
||||||
|
<ElRadioGroup v-model="setWiFiForm.enabled">
|
||||||
|
<ElRadio :value="1">启用</ElRadio>
|
||||||
|
<ElRadio :value="0">禁用</ElRadio>
|
||||||
|
</ElRadioGroup>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="WiFi名称" prop="ssid">
|
||||||
|
<ElInput
|
||||||
|
v-model="setWiFiForm.ssid"
|
||||||
|
placeholder="请输入WiFi名称(1-32个字符)"
|
||||||
|
maxlength="32"
|
||||||
|
show-word-limit
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="WiFi密码" prop="password">
|
||||||
|
<ElInput
|
||||||
|
v-model="setWiFiForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入WiFi密码(8-63个字符)"
|
||||||
|
maxlength="63"
|
||||||
|
show-word-limit
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="setWiFiDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleConfirmSetWiFi" :loading="setWiFiLoading">
|
||||||
|
确认设置
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -403,7 +524,17 @@
|
|||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { DeviceService, ShopService, CardService, PackageSeriesService } from '@/api/modules'
|
import { DeviceService, ShopService, CardService, PackageSeriesService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElIcon, ElTreeSelect } from 'element-plus'
|
import {
|
||||||
|
ElMessage,
|
||||||
|
ElMessageBox,
|
||||||
|
ElTag,
|
||||||
|
ElSwitch,
|
||||||
|
ElIcon,
|
||||||
|
ElTreeSelect,
|
||||||
|
ElInputNumber,
|
||||||
|
ElRadioGroup,
|
||||||
|
ElRadio
|
||||||
|
} from 'element-plus'
|
||||||
import { Loading } from '@element-plus/icons-vue'
|
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 {
|
||||||
@@ -416,6 +547,8 @@
|
|||||||
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'
|
||||||
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText } from '@/config/constants'
|
import { CommonStatus, getStatusText } from '@/config/constants'
|
||||||
import type { PackageSeriesResponse } from '@/types/api'
|
import type { PackageSeriesResponse } from '@/types/api'
|
||||||
@@ -473,6 +606,59 @@
|
|||||||
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
|
slot_position: [{ required: true, message: '请选择插槽位置', trigger: 'change' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设备操作相关对话框
|
||||||
|
const speedLimitDialogVisible = ref(false)
|
||||||
|
const speedLimitLoading = ref(false)
|
||||||
|
const speedLimitFormRef = ref<FormInstance>()
|
||||||
|
const speedLimitForm = reactive({
|
||||||
|
download_speed: 1024,
|
||||||
|
upload_speed: 512
|
||||||
|
})
|
||||||
|
const speedLimitRules = reactive<FormRules>({
|
||||||
|
download_speed: [
|
||||||
|
{ required: true, message: '请输入下行速率', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
upload_speed: [
|
||||||
|
{ required: true, message: '请输入上行速率', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 1, message: '速率不能小于1KB/s', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const currentOperatingDevice = ref<string>('')
|
||||||
|
|
||||||
|
// 设备操作右键菜单
|
||||||
|
const deviceOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentOperatingDeviceNo = ref<string>('')
|
||||||
|
|
||||||
|
const switchCardDialogVisible = ref(false)
|
||||||
|
const switchCardLoading = ref(false)
|
||||||
|
const switchCardFormRef = ref<FormInstance>()
|
||||||
|
const switchCardForm = reactive({
|
||||||
|
target_iccid: ''
|
||||||
|
})
|
||||||
|
const switchCardRules = reactive<FormRules>({
|
||||||
|
target_iccid: [{ required: true, message: '请输入目标ICCID', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const setWiFiDialogVisible = ref(false)
|
||||||
|
const setWiFiLoading = ref(false)
|
||||||
|
const setWiFiFormRef = ref<FormInstance>()
|
||||||
|
const setWiFiForm = reactive({
|
||||||
|
enabled: 1,
|
||||||
|
ssid: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
const setWiFiRules = reactive<FormRules>({
|
||||||
|
ssid: [
|
||||||
|
{ required: true, message: '请输入WiFi名称', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 32, message: 'WiFi名称长度为1-32个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入WiFi密码', trigger: 'blur' },
|
||||||
|
{ min: 8, max: 63, message: 'WiFi密码长度为8-63个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
device_no: '',
|
device_no: '',
|
||||||
@@ -559,7 +745,6 @@
|
|||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ID', prop: 'id' },
|
|
||||||
{ label: '设备号', prop: 'device_no' },
|
{ label: '设备号', prop: 'device_no' },
|
||||||
{ label: '设备名称', prop: 'device_name' },
|
{ label: '设备名称', prop: 'device_name' },
|
||||||
{ label: '设备型号', prop: 'device_model' },
|
{ label: '设备型号', prop: 'device_model' },
|
||||||
@@ -794,15 +979,11 @@
|
|||||||
|
|
||||||
// 动态列配置
|
// 动态列配置
|
||||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||||
{
|
|
||||||
prop: 'id',
|
|
||||||
label: 'ID',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'device_no',
|
prop: 'device_no',
|
||||||
label: '设备号',
|
label: '设备号',
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
showOverflowTooltip: true,
|
||||||
formatter: (row: Device) => {
|
formatter: (row: Device) => {
|
||||||
return h(
|
return h(
|
||||||
'span',
|
'span',
|
||||||
@@ -827,7 +1008,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'device_type',
|
prop: 'device_type',
|
||||||
label: '设备类型',
|
label: '设备类型',
|
||||||
width: 100
|
width: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'manufacturer',
|
prop: 'manufacturer',
|
||||||
@@ -868,7 +1049,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'batch_no',
|
prop: 'batch_no',
|
||||||
label: '批次号',
|
label: '批次号',
|
||||||
minWidth: 160,
|
minWidth: 180,
|
||||||
formatter: (row: Device) => row.batch_no || '-'
|
formatter: (row: Device) => row.batch_no || '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -880,17 +1061,17 @@
|
|||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 180,
|
width: 200,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: Device) => {
|
formatter: (row: Device) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
return h('div', { style: 'display: flex; gap: 0; align-items: center;' }, [
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
text: '查看卡片',
|
text: '查看卡片',
|
||||||
onClick: () => handleViewCards(row)
|
onClick: () => handleViewCards(row)
|
||||||
}),
|
}),
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
type: 'delete',
|
text: '更多操作',
|
||||||
onClick: () => deleteDevice(row)
|
onContextmenu: (e: MouseEvent) => showDeviceOperationMenu(e, row.device_no)
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -1220,6 +1401,249 @@
|
|||||||
seriesBindingFormRef.value.resetFields()
|
seriesBindingFormRef.value.resetFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 设备操作相关 ==========
|
||||||
|
|
||||||
|
// 设备操作路由
|
||||||
|
const handleDeviceOperation = (command: string, deviceNo: string) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'reboot':
|
||||||
|
handleRebootDevice(deviceNo)
|
||||||
|
break
|
||||||
|
case 'reset':
|
||||||
|
handleResetDevice(deviceNo)
|
||||||
|
break
|
||||||
|
case 'speed-limit':
|
||||||
|
showSpeedLimitDialog(deviceNo)
|
||||||
|
break
|
||||||
|
case 'switch-card':
|
||||||
|
showSwitchCardDialog(deviceNo)
|
||||||
|
break
|
||||||
|
case 'set-wifi':
|
||||||
|
showSetWiFiDialog(deviceNo)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
handleDeleteDeviceByNo(deviceNo)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过设备号删除设备
|
||||||
|
const handleDeleteDeviceByNo = async (deviceNo: string) => {
|
||||||
|
// 先根据设备号找到设备对象
|
||||||
|
const device = deviceList.value.find(d => d.device_no === deviceNo)
|
||||||
|
if (device) {
|
||||||
|
deleteDevice(device)
|
||||||
|
} else {
|
||||||
|
ElMessage.error('未找到该设备')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启设备
|
||||||
|
const handleRebootDevice = (imei: string) => {
|
||||||
|
ElMessageBox.confirm(`确定要重启设备 ${imei} 吗?`, '重启确认', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.rebootDevice(imei)
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('重启指令已发送')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '重启失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('重启设备失败:', error)
|
||||||
|
ElMessage.error(error?.message || '重启失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复出厂设置
|
||||||
|
const handleResetDevice = (imei: string) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要恢复设备 ${imei} 的出厂设置吗?此操作将清除所有配置和数据!`,
|
||||||
|
'恢复出厂设置确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.resetDevice(imei)
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('恢复出厂设置指令已发送')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('恢复出厂设置失败:', error)
|
||||||
|
ElMessage.error(error?.message || '操作失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示设置限速对话框
|
||||||
|
const showSpeedLimitDialog = (imei: string) => {
|
||||||
|
currentOperatingDevice.value = imei
|
||||||
|
speedLimitForm.download_speed = 1024
|
||||||
|
speedLimitForm.upload_speed = 512
|
||||||
|
speedLimitDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认设置限速
|
||||||
|
const handleConfirmSpeedLimit = async () => {
|
||||||
|
if (!speedLimitFormRef.value) return
|
||||||
|
|
||||||
|
await speedLimitFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
speedLimitLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.setSpeedLimit(currentOperatingDevice.value, {
|
||||||
|
download_speed: speedLimitForm.download_speed,
|
||||||
|
upload_speed: speedLimitForm.upload_speed
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('限速设置成功')
|
||||||
|
speedLimitDialogVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '设置失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('设置限速失败:', error)
|
||||||
|
ElMessage.error(error?.message || '设置失败')
|
||||||
|
} finally {
|
||||||
|
speedLimitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示切换SIM卡对话框
|
||||||
|
const showSwitchCardDialog = (imei: string) => {
|
||||||
|
currentOperatingDevice.value = imei
|
||||||
|
switchCardForm.target_iccid = ''
|
||||||
|
switchCardDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认切换SIM卡
|
||||||
|
const handleConfirmSwitchCard = async () => {
|
||||||
|
if (!switchCardFormRef.value) return
|
||||||
|
|
||||||
|
await switchCardFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
switchCardLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.switchCard(currentOperatingDevice.value, {
|
||||||
|
target_iccid: switchCardForm.target_iccid
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('切换SIM卡指令已发送')
|
||||||
|
switchCardDialogVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '切换失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('切换SIM卡失败:', error)
|
||||||
|
ElMessage.error(error?.message || '切换失败')
|
||||||
|
} finally {
|
||||||
|
switchCardLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示设置WiFi对话框
|
||||||
|
const showSetWiFiDialog = (imei: string) => {
|
||||||
|
currentOperatingDevice.value = imei
|
||||||
|
setWiFiForm.enabled = 1
|
||||||
|
setWiFiForm.ssid = ''
|
||||||
|
setWiFiForm.password = ''
|
||||||
|
setWiFiDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认设置WiFi
|
||||||
|
const handleConfirmSetWiFi = async () => {
|
||||||
|
if (!setWiFiFormRef.value) return
|
||||||
|
|
||||||
|
await setWiFiFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
setWiFiLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await DeviceService.setWiFi(currentOperatingDevice.value, {
|
||||||
|
enabled: setWiFiForm.enabled,
|
||||||
|
ssid: setWiFiForm.ssid,
|
||||||
|
password: setWiFiForm.password
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('WiFi设置成功')
|
||||||
|
setWiFiDialogVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '设置失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('设置WiFi失败:', error)
|
||||||
|
ElMessage.error(error?.message || '设置失败')
|
||||||
|
} finally {
|
||||||
|
setWiFiLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备操作菜单项配置
|
||||||
|
const deviceOperationMenuItems = computed((): MenuItemType[] => [
|
||||||
|
{
|
||||||
|
key: 'reboot',
|
||||||
|
label: '重启设备'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reset',
|
||||||
|
label: '恢复出厂'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'speed-limit',
|
||||||
|
label: '设置限速'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'switch-card',
|
||||||
|
label: '切换SIM卡'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'set-wifi',
|
||||||
|
label: '设置WiFi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除设备'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 显示设备操作菜单
|
||||||
|
const showDeviceOperationMenu = (e: MouseEvent, deviceNo: string) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
currentOperatingDeviceNo.value = deviceNo
|
||||||
|
deviceOperationMenuRef.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理设备操作菜单选择
|
||||||
|
const handleDeviceOperationMenuSelect = (item: MenuItemType) => {
|
||||||
|
const deviceNo = currentOperatingDeviceNo.value
|
||||||
|
if (!deviceNo) return
|
||||||
|
|
||||||
|
handleDeviceOperation(item.key, deviceNo)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -399,9 +399,6 @@
|
|||||||
getCarrierTypeText(currentCardDetail.carrier_type)
|
getCarrierTypeText(currentCardDetail.carrier_type)
|
||||||
}}</ElDescriptionsItem>
|
}}</ElDescriptionsItem>
|
||||||
|
|
||||||
<ElDescriptionsItem label="卡类型">{{
|
|
||||||
currentCardDetail.card_type || '--'
|
|
||||||
}}</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="卡业务类型">{{
|
<ElDescriptionsItem label="卡业务类型">{{
|
||||||
getCardCategoryText(currentCardDetail.card_category)
|
getCardCategoryText(currentCardDetail.card_category)
|
||||||
}}</ElDescriptionsItem>
|
}}</ElDescriptionsItem>
|
||||||
@@ -437,9 +434,9 @@
|
|||||||
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
>{{ currentCardDetail.data_usage_mb }} MB</ElDescriptionsItem
|
||||||
>
|
>
|
||||||
|
|
||||||
<ElDescriptionsItem label="首次佣金">
|
<ElDescriptionsItem label="一次性佣金">
|
||||||
<ElTag :type="currentCardDetail.first_commission_paid ? 'success' : 'info'">
|
<ElTag :type="currentCardDetail.first_commission_paid ? 'success' : 'info'">
|
||||||
{{ currentCardDetail.first_commission_paid ? '已支付' : '未支付' }}
|
{{ currentCardDetail.first_commission_paid ? '已产生' : '未产生' }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="累计充值">{{
|
<ElDescriptionsItem label="累计充值">{{
|
||||||
@@ -463,6 +460,111 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 流量使用查询对话框 -->
|
||||||
|
<ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px">
|
||||||
|
<div v-if="flowUsageLoading" 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="flowUsageData" :column="1" border>
|
||||||
|
<ElDescriptionsItem label="已用流量">{{
|
||||||
|
flowUsageData.usedFlow || 0
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="流量单位">{{
|
||||||
|
flowUsageData.unit || 'MB'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem v-if="flowUsageData.extend" label="扩展信息">{{
|
||||||
|
flowUsageData.extend
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton type="primary" @click="flowUsageDialogVisible = false">关闭</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 实名状态查询对话框 -->
|
||||||
|
<ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px">
|
||||||
|
<div v-if="realnameStatusLoading" 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="realnameStatusData" :column="1" border>
|
||||||
|
<ElDescriptionsItem label="实名状态">{{
|
||||||
|
realnameStatusData.status || '未知'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem v-if="realnameStatusData.extend" label="扩展信息">{{
|
||||||
|
realnameStatusData.extend
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton type="primary" @click="realnameStatusDialogVisible = false">关闭</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 卡实时状态查询对话框 -->
|
||||||
|
<ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px">
|
||||||
|
<div v-if="cardStatusLoading" 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="cardStatusData" :column="1" border>
|
||||||
|
<ElDescriptionsItem label="ICCID">{{
|
||||||
|
cardStatusData.iccid || '--'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="卡状态">{{
|
||||||
|
cardStatusData.cardStatus || '未知'
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem v-if="cardStatusData.extend" label="扩展信息">{{
|
||||||
|
cardStatusData.extend
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton type="primary" @click="cardStatusDialogVisible = false">关闭</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 实名认证链接对话框 -->
|
||||||
|
<ElDialog v-model="realnameLinkDialogVisible" title="实名认证链接" width="500px">
|
||||||
|
<div v-if="realnameLinkLoading" style="text-align: center; padding: 40px">
|
||||||
|
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
|
||||||
|
<div style="margin-top: 16px">获取中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="realnameLinkData && realnameLinkData.link" style="text-align: center">
|
||||||
|
<div style="margin-bottom: 16px">
|
||||||
|
<img v-if="qrcodeDataURL" :src="qrcodeDataURL" alt="实名认证二维码" />
|
||||||
|
</div>
|
||||||
|
<ElDescriptions :column="1" border>
|
||||||
|
<ElDescriptionsItem label="实名链接">
|
||||||
|
<a :href="realnameLinkData.link" target="_blank" style="color: var(--el-color-primary)">
|
||||||
|
{{ realnameLinkData.link }}
|
||||||
|
</a>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem v-if="realnameLinkData.extend" label="扩展信息">{{
|
||||||
|
realnameLinkData.extend
|
||||||
|
}}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton type="primary" @click="realnameLinkDialogVisible = false">关闭</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
<!-- 更多操作右键菜单 -->
|
<!-- 更多操作右键菜单 -->
|
||||||
<ArtMenuRight
|
<ArtMenuRight
|
||||||
ref="moreMenuRef"
|
ref="moreMenuRef"
|
||||||
@@ -470,6 +572,14 @@
|
|||||||
:menu-width="180"
|
:menu-width="180"
|
||||||
@select="handleMoreMenuSelect"
|
@select="handleMoreMenuSelect"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 表格行操作右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="cardOperationMenuRef"
|
||||||
|
:menu-items="cardOperationMenuItems"
|
||||||
|
:menu-width="160"
|
||||||
|
@select="handleCardOperationMenuSelect"
|
||||||
|
/>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -479,14 +589,16 @@
|
|||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { CardService, ShopService, PackageSeriesService } from '@/api/modules'
|
import { CardService, ShopService, PackageSeriesService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag, ElIcon } from 'element-plus'
|
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
|
||||||
import { Loading } from '@element-plus/icons-vue'
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import QRCode from 'qrcode'
|
||||||
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 ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import type {
|
import type {
|
||||||
StandaloneIotCard,
|
StandaloneIotCard,
|
||||||
StandaloneCardStatus,
|
StandaloneCardStatus,
|
||||||
@@ -543,8 +655,28 @@
|
|||||||
const cardDetailLoading = ref(false)
|
const cardDetailLoading = ref(false)
|
||||||
const currentCardDetail = ref<any>(null)
|
const currentCardDetail = ref<any>(null)
|
||||||
|
|
||||||
|
// IoT卡操作相关对话框
|
||||||
|
const flowUsageDialogVisible = ref(false)
|
||||||
|
const flowUsageLoading = ref(false)
|
||||||
|
const flowUsageData = ref<any>(null)
|
||||||
|
|
||||||
|
const realnameStatusDialogVisible = ref(false)
|
||||||
|
const realnameStatusLoading = ref(false)
|
||||||
|
const realnameStatusData = ref<any>(null)
|
||||||
|
|
||||||
|
const cardStatusDialogVisible = ref(false)
|
||||||
|
const cardStatusLoading = ref(false)
|
||||||
|
const cardStatusData = ref<any>(null)
|
||||||
|
|
||||||
|
const realnameLinkDialogVisible = ref(false)
|
||||||
|
const realnameLinkLoading = ref(false)
|
||||||
|
const realnameLinkData = ref<any>(null)
|
||||||
|
const qrcodeDataURL = ref<string>('')
|
||||||
|
|
||||||
// 更多操作右键菜单
|
// 更多操作右键菜单
|
||||||
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
const moreMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const cardOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentOperatingIccid = ref<string>('')
|
||||||
|
|
||||||
// 店铺相关
|
// 店铺相关
|
||||||
const targetShopLoading = ref(false)
|
const targetShopLoading = ref(false)
|
||||||
@@ -728,9 +860,9 @@
|
|||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
{ label: 'ICCID', prop: 'iccid' },
|
{ label: 'ICCID', prop: 'iccid' },
|
||||||
{ label: '卡接入号', prop: 'msisdn' },
|
{ label: '卡接入号', prop: 'msisdn' },
|
||||||
{ label: '卡类型', prop: 'card_type' },
|
|
||||||
{ label: '卡业务类型', prop: 'card_category' },
|
{ label: '卡业务类型', prop: 'card_category' },
|
||||||
{ label: '运营商', prop: 'carrier_name' },
|
{ label: '运营商', prop: 'carrier_name' },
|
||||||
|
{ label: '店铺名称', prop: 'shop_name' },
|
||||||
{ label: '成本价', prop: 'cost_price' },
|
{ label: '成本价', prop: 'cost_price' },
|
||||||
{ label: '分销价', prop: 'distribute_price' },
|
{ label: '分销价', prop: 'distribute_price' },
|
||||||
{ label: '状态', prop: 'status' },
|
{ label: '状态', prop: 'status' },
|
||||||
@@ -738,7 +870,7 @@
|
|||||||
{ label: '网络状态', prop: 'network_status' },
|
{ label: '网络状态', prop: 'network_status' },
|
||||||
{ label: '实名状态', prop: 'real_name_status' },
|
{ label: '实名状态', prop: 'real_name_status' },
|
||||||
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
|
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
|
||||||
{ label: '首次佣金', prop: 'first_commission_paid' },
|
{ label: '一次性佣金', prop: 'first_commission_paid' },
|
||||||
{ label: '累计充值', prop: 'accumulated_recharge' },
|
{ label: '累计充值', prop: 'accumulated_recharge' },
|
||||||
{ label: '创建时间', prop: 'created_at' }
|
{ label: '创建时间', prop: 'created_at' }
|
||||||
]
|
]
|
||||||
@@ -865,21 +997,23 @@
|
|||||||
label: '卡接入号',
|
label: '卡接入号',
|
||||||
width: 130
|
width: 130
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'card_type',
|
|
||||||
label: '卡类型',
|
|
||||||
width: 100
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
prop: 'card_category',
|
prop: 'card_category',
|
||||||
label: '卡业务类型',
|
label: '卡业务类型',
|
||||||
width: 100
|
width: 100,
|
||||||
|
formatter: (row: StandaloneIotCard) => getCardCategoryText(row.card_category)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'carrier_name',
|
prop: 'carrier_name',
|
||||||
label: '运营商',
|
label: '运营商',
|
||||||
width: 150
|
width: 150
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'shop_name',
|
||||||
|
label: '店铺名称',
|
||||||
|
minWidth: 150,
|
||||||
|
formatter: (row: StandaloneIotCard) => row.shop_name || '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 'cost_price',
|
prop: 'cost_price',
|
||||||
label: '成本价',
|
label: '成本价',
|
||||||
@@ -937,11 +1071,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'first_commission_paid',
|
prop: 'first_commission_paid',
|
||||||
label: '首次佣金',
|
label: '一次性佣金',
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: StandaloneIotCard) => {
|
formatter: (row: StandaloneIotCard) => {
|
||||||
const type = row.first_commission_paid ? 'success' : 'info'
|
const type = row.first_commission_paid ? 'success' : 'info'
|
||||||
const text = row.first_commission_paid ? '已支付' : '未支付'
|
const text = row.first_commission_paid ? '已产生' : '未产生'
|
||||||
return h(ElTag, { type, size: 'small' }, () => text)
|
return h(ElTag, { type, size: 'small' }, () => text)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -956,6 +1090,24 @@
|
|||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
width: 180,
|
width: 180,
|
||||||
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
|
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'operation',
|
||||||
|
label: '操作',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: (row: StandaloneIotCard) => {
|
||||||
|
return h('div', { style: 'display: flex; gap: 0; align-items: center;' }, [
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '查询流量',
|
||||||
|
onClick: () => showFlowUsageDialog(row.iccid)
|
||||||
|
}),
|
||||||
|
h(ArtButtonTable, {
|
||||||
|
text: '更多操作',
|
||||||
|
onContextmenu: (e: MouseEvent) => showCardOperationMenu(e, row.iccid)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1364,28 +1516,47 @@
|
|||||||
const moreMenuItems = computed((): MenuItemType[] => [
|
const moreMenuItems = computed((): MenuItemType[] => [
|
||||||
{
|
{
|
||||||
key: 'distribution',
|
key: 'distribution',
|
||||||
label: '网卡分销',
|
label: '网卡分销'
|
||||||
icon: ''
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'recharge',
|
key: 'recharge',
|
||||||
label: '批量充值',
|
label: '批量充值'
|
||||||
icon: ''
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'recycle',
|
key: 'recycle',
|
||||||
label: '网卡回收',
|
label: '网卡回收'
|
||||||
icon: ''
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'download',
|
key: 'download',
|
||||||
label: '批量下载',
|
label: '批量下载'
|
||||||
icon: ''
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'changePackage',
|
key: 'changePackage',
|
||||||
label: '变更套餐',
|
label: '变更套餐'
|
||||||
icon: ''
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 卡操作菜单项配置
|
||||||
|
const cardOperationMenuItems = computed((): MenuItemType[] => [
|
||||||
|
{
|
||||||
|
key: 'realname-status',
|
||||||
|
label: '查询实名状态'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'card-status',
|
||||||
|
label: '查询卡状态'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realname-link',
|
||||||
|
label: '获取实名链接'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'start-card',
|
||||||
|
label: '启用卡片'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stop-card',
|
||||||
|
label: '停用卡片'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1417,6 +1588,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示卡操作菜单
|
||||||
|
const showCardOperationMenu = (e: MouseEvent, iccid: string) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
currentOperatingIccid.value = iccid
|
||||||
|
cardOperationMenuRef.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理卡操作菜单选择
|
||||||
|
const handleCardOperationMenuSelect = (item: MenuItemType) => {
|
||||||
|
const iccid = currentOperatingIccid.value
|
||||||
|
if (!iccid) return
|
||||||
|
|
||||||
|
handleCardOperation(item.key, iccid)
|
||||||
|
}
|
||||||
|
|
||||||
// 网卡分销 - 正在开发中
|
// 网卡分销 - 正在开发中
|
||||||
const cardDistribution = () => {
|
const cardDistribution = () => {
|
||||||
ElMessage.info('功能正在开发中')
|
ElMessage.info('功能正在开发中')
|
||||||
@@ -1441,6 +1628,177 @@
|
|||||||
const changePackage = () => {
|
const changePackage = () => {
|
||||||
ElMessage.info('功能正在开发中')
|
ElMessage.info('功能正在开发中')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IoT卡操作处理函数
|
||||||
|
const handleCardOperation = (command: string, iccid: string) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'realname-status':
|
||||||
|
showRealnameStatusDialog(iccid)
|
||||||
|
break
|
||||||
|
case 'card-status':
|
||||||
|
showCardStatusDialog(iccid)
|
||||||
|
break
|
||||||
|
case 'realname-link':
|
||||||
|
showRealnameLinkDialog(iccid)
|
||||||
|
break
|
||||||
|
case 'start-card':
|
||||||
|
handleStartCard(iccid)
|
||||||
|
break
|
||||||
|
case 'stop-card':
|
||||||
|
handleStopCard(iccid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询流量使用
|
||||||
|
const showFlowUsageDialog = async (iccid: string) => {
|
||||||
|
flowUsageDialogVisible.value = true
|
||||||
|
flowUsageLoading.value = true
|
||||||
|
flowUsageData.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await CardService.getGatewayFlow(iccid)
|
||||||
|
if (res.code === 0) {
|
||||||
|
flowUsageData.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '查询失败')
|
||||||
|
flowUsageDialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('查询流量使用失败:', error)
|
||||||
|
ElMessage.error(error?.message || '查询失败')
|
||||||
|
flowUsageDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
flowUsageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询实名认证状态
|
||||||
|
const showRealnameStatusDialog = async (iccid: string) => {
|
||||||
|
realnameStatusDialogVisible.value = true
|
||||||
|
realnameStatusLoading.value = true
|
||||||
|
realnameStatusData.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await CardService.getGatewayRealname(iccid)
|
||||||
|
if (res.code === 0) {
|
||||||
|
realnameStatusData.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '查询失败')
|
||||||
|
realnameStatusDialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('查询实名状态失败:', error)
|
||||||
|
ElMessage.error(error?.message || '查询失败')
|
||||||
|
realnameStatusDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
realnameStatusLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询卡实时状态
|
||||||
|
const showCardStatusDialog = async (iccid: string) => {
|
||||||
|
cardStatusDialogVisible.value = true
|
||||||
|
cardStatusLoading.value = true
|
||||||
|
cardStatusData.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await CardService.getGatewayStatus(iccid)
|
||||||
|
if (res.code === 0) {
|
||||||
|
cardStatusData.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '查询失败')
|
||||||
|
cardStatusDialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('查询卡状态失败:', error)
|
||||||
|
ElMessage.error(error?.message || '查询失败')
|
||||||
|
cardStatusDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
cardStatusLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实名认证链接
|
||||||
|
const showRealnameLinkDialog = async (iccid: string) => {
|
||||||
|
realnameLinkDialogVisible.value = true
|
||||||
|
realnameLinkLoading.value = true
|
||||||
|
realnameLinkData.value = null
|
||||||
|
qrcodeDataURL.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await CardService.getRealnameLink(iccid)
|
||||||
|
if (res.code === 0 && res.data?.link) {
|
||||||
|
realnameLinkData.value = res.data
|
||||||
|
// 生成二维码
|
||||||
|
qrcodeDataURL.value = await QRCode.toDataURL(res.data.link, {
|
||||||
|
width: 200,
|
||||||
|
margin: 1
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '获取失败')
|
||||||
|
realnameLinkDialogVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取实名链接失败:', error)
|
||||||
|
ElMessage.error(error?.message || '获取失败')
|
||||||
|
realnameLinkDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
realnameLinkLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用卡片(复机)
|
||||||
|
const handleStartCard = (iccid: string) => {
|
||||||
|
ElMessageBox.confirm('确定要启用该卡片吗?', '确认启用', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await CardService.startCard(iccid)
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('启用成功')
|
||||||
|
getTableData()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '启用失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('启用卡片失败:', error)
|
||||||
|
ElMessage.error(error?.message || '启用失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户取消
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停用卡片(停机)
|
||||||
|
const handleStopCard = (iccid: string) => {
|
||||||
|
ElMessageBox.confirm('确定要停用该卡片吗?', '确认停用', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await CardService.stopCard(iccid)
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('停用成功')
|
||||||
|
getTableData()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '停用失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('停用卡片失败:', error)
|
||||||
|
ElMessage.error(error?.message || '停用失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户取消
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -66,10 +66,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElFormItem label="运营商" required style="margin-bottom: 20px">
|
<ElFormItem label="运营商" required style="margin-bottom: 20px">
|
||||||
<ElSelect v-model="selectedCarrierId" placeholder="请选择运营商" style="width: 100%">
|
<ElSelect
|
||||||
<ElOption label="中国移动" :value="1" />
|
v-model="selectedCarrierId"
|
||||||
<ElOption label="中国联通" :value="2" />
|
placeholder="请输入运营商名称搜索"
|
||||||
<ElOption label="中国电信" :value="3" />
|
style="width: 100%"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
:remote-method="handleCarrierSearch"
|
||||||
|
:loading="carrierLoading"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="carrier in carrierList"
|
||||||
|
:key="carrier.id"
|
||||||
|
:label="carrier.carrier_name"
|
||||||
|
:value="carrier.id"
|
||||||
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
@@ -194,7 +206,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { CardService } from '@/api/modules'
|
import { CardService, CarrierService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag, ElFormItem, ElSelect, ElOption } from 'element-plus'
|
import { ElMessage, ElTag, ElFormItem, ElSelect, ElOption } from 'element-plus'
|
||||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||||
import type { UploadInstance } from 'element-plus'
|
import type { UploadInstance } from 'element-plus'
|
||||||
@@ -204,6 +216,7 @@
|
|||||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||||
import { StorageService } from '@/api/modules/storage'
|
import { StorageService } from '@/api/modules/storage'
|
||||||
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
import type { IotCardImportTask, IotCardImportTaskStatus } from '@/types/api/card'
|
||||||
|
import type { Carrier } from '@/types/api'
|
||||||
|
|
||||||
defineOptions({ name: 'IotCardTask' })
|
defineOptions({ name: 'IotCardTask' })
|
||||||
|
|
||||||
@@ -215,6 +228,8 @@
|
|||||||
const importDialogVisible = ref(false)
|
const importDialogVisible = ref(false)
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const selectedCarrierId = ref<number>()
|
const selectedCarrierId = ref<number>()
|
||||||
|
const carrierList = ref<Carrier[]>([])
|
||||||
|
const carrierLoading = ref(false)
|
||||||
|
|
||||||
// 搜索表单初始值
|
// 搜索表单初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
@@ -235,7 +250,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 搜索表单配置
|
// 搜索表单配置
|
||||||
const searchFormItems: SearchFormItem[] = [
|
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: '任务状态',
|
label: '任务状态',
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
@@ -255,15 +270,17 @@
|
|||||||
label: '运营商',
|
label: '运营商',
|
||||||
prop: 'carrier_id',
|
prop: 'carrier_id',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
|
options: carrierList.value.map((carrier) => ({
|
||||||
|
label: carrier.carrier_name,
|
||||||
|
value: carrier.id
|
||||||
|
})),
|
||||||
config: {
|
config: {
|
||||||
clearable: true,
|
clearable: true,
|
||||||
placeholder: '全部'
|
filterable: true,
|
||||||
},
|
remote: true,
|
||||||
options: () => [
|
remoteMethod: handleCarrierSearch,
|
||||||
{ label: '中国移动', value: 1 },
|
placeholder: '请输入运营商名称搜索'
|
||||||
{ label: '中国联通', value: 2 },
|
}
|
||||||
{ label: '中国电信', value: 3 }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '批次号',
|
label: '批次号',
|
||||||
@@ -285,7 +302,7 @@
|
|||||||
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
])
|
||||||
|
|
||||||
// 列配置
|
// 列配置
|
||||||
const columnOptions = [
|
const columnOptions = [
|
||||||
@@ -456,6 +473,7 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTableData()
|
getTableData()
|
||||||
|
loadCarrierList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取IoT卡任务列表
|
// 获取IoT卡任务列表
|
||||||
@@ -717,6 +735,31 @@
|
|||||||
importDialogVisible.value = false
|
importDialogVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载运营商列表
|
||||||
|
const loadCarrierList = async (carrierName?: string) => {
|
||||||
|
carrierLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await CarrierService.getCarriers({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
carrier_name: carrierName || undefined,
|
||||||
|
status: 1 // 只加载启用的运营商
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
carrierList.value = res.data.items || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取运营商列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
carrierLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运营商搜索处理
|
||||||
|
const handleCarrierSearch = (query: string) => {
|
||||||
|
loadCarrierList(query)
|
||||||
|
}
|
||||||
|
|
||||||
// 提交上传
|
// 提交上传
|
||||||
const submitUpload = async () => {
|
const submitUpload = async () => {
|
||||||
if (!selectedCarrierId.value) {
|
if (!selectedCarrierId.value) {
|
||||||
|
|||||||
@@ -140,12 +140,15 @@
|
|||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="成本价(分)" prop="cost_price">
|
<ElFormItem label="成本价(元)" prop="cost_price">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="form.cost_price"
|
v-model="form.cost_price"
|
||||||
:min="0"
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
placeholder="请输入成本价"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
@@ -290,7 +293,7 @@
|
|||||||
validator: (rule: any, value: any, callback: any) => {
|
validator: (rule: any, value: any, callback: any) => {
|
||||||
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 / 100) {
|
||||||
callback(
|
callback(
|
||||||
new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`)
|
new Error(`成本价不能低于套餐价格 ¥${(form.package_base_price / 100).toFixed(2)}`)
|
||||||
)
|
)
|
||||||
@@ -617,7 +620,7 @@
|
|||||||
form.id = row.id
|
form.id = row.id
|
||||||
form.package_id = row.package_id
|
form.package_id = row.package_id
|
||||||
form.shop_id = row.shop_id
|
form.shop_id = row.shop_id
|
||||||
form.cost_price = row.cost_price
|
form.cost_price = row.cost_price / 100 // 转换为元显示
|
||||||
form.package_base_price = 0
|
form.package_base_price = 0
|
||||||
} else {
|
} else {
|
||||||
form.id = 0
|
form.id = 0
|
||||||
@@ -639,9 +642,9 @@
|
|||||||
// 从套餐选项中找到选中的套餐
|
// 从套餐选项中找到选中的套餐
|
||||||
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 / 100
|
||||||
form.package_base_price = selectedPackage.price
|
form.package_base_price = selectedPackage.price // 保持原始值(分)用于验证
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 清空时重置成本价
|
// 清空时重置成本价
|
||||||
@@ -695,10 +698,13 @@
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
// 将元转换为分提交给后端
|
||||||
|
const costPriceInCents = Math.round(form.cost_price * 100)
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
package_id: form.package_id,
|
package_id: form.package_id,
|
||||||
shop_id: form.shop_id,
|
shop_id: form.shop_id,
|
||||||
cost_price: form.cost_price
|
cost_price: costPriceInCents
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogType.value === 'add') {
|
if (dialogType.value === 'add') {
|
||||||
@@ -706,7 +712,7 @@
|
|||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
} else {
|
} else {
|
||||||
await ShopPackageAllocationService.updateShopPackageAllocation(form.id, {
|
await ShopPackageAllocationService.updateShopPackageAllocation(form.id, {
|
||||||
cost_price: form.cost_price
|
cost_price: costPriceInCents
|
||||||
})
|
})
|
||||||
ElMessage.success('修改成功')
|
ElMessage.success('修改成功')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,12 +50,18 @@
|
|||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<ElFormItem label="套餐编码" prop="package_code">
|
<ElFormItem label="套餐编码" prop="package_code">
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="form.package_code"
|
v-model="form.package_code"
|
||||||
placeholder="请输入套餐编码"
|
placeholder="请输入套餐编码或点击生成"
|
||||||
:disabled="dialogType === 'edit'"
|
:disabled="dialogType === 'edit'"
|
||||||
clearable
|
clearable
|
||||||
|
style="flex: 1;"
|
||||||
/>
|
/>
|
||||||
|
<ElButton v-if="dialogType === 'add'" @click="handleGeneratePackageCode">
|
||||||
|
生成编码
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="套餐名称" prop="package_name">
|
<ElFormItem label="套餐名称" prop="package_name">
|
||||||
<ElInput v-model="form.package_name" placeholder="请输入套餐名称" clearable />
|
<ElInput v-model="form.package_name" placeholder="请输入套餐名称" clearable />
|
||||||
@@ -165,7 +171,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { PackageManageService, PackageSeriesService } from '@/api/modules'
|
import { PackageManageService, PackageSeriesService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
|
import type { PackageResponse, SeriesSelectOption } from '@/types/api'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
@@ -186,6 +192,7 @@
|
|||||||
getDataTypeTag,
|
getDataTypeTag,
|
||||||
getShelfStatusText
|
getShelfStatusText
|
||||||
} from '@/config/constants'
|
} from '@/config/constants'
|
||||||
|
import { generatePackageCode } from '@/utils/codeGenerator'
|
||||||
|
|
||||||
defineOptions({ name: 'PackageList' })
|
defineOptions({ name: 'PackageList' })
|
||||||
|
|
||||||
@@ -651,6 +658,12 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成套餐编码
|
||||||
|
const handleGeneratePackageCode = () => {
|
||||||
|
form.package_code = generatePackageCode()
|
||||||
|
ElMessage.success('编码生成成功')
|
||||||
|
}
|
||||||
|
|
||||||
// 处理弹窗关闭事件
|
// 处理弹窗关闭事件
|
||||||
const handleDialogClosed = () => {
|
const handleDialogClosed = () => {
|
||||||
// 清除表单验证状态
|
// 清除表单验证状态
|
||||||
|
|||||||
@@ -49,12 +49,18 @@
|
|||||||
>
|
>
|
||||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<ElFormItem label="系列编码" prop="series_code">
|
<ElFormItem label="系列编码" prop="series_code">
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="form.series_code"
|
v-model="form.series_code"
|
||||||
placeholder="请输入系列编码"
|
placeholder="请输入系列编码或点击生成"
|
||||||
:disabled="dialogType === 'edit'"
|
:disabled="dialogType === 'edit'"
|
||||||
clearable
|
clearable
|
||||||
|
style="flex: 1;"
|
||||||
/>
|
/>
|
||||||
|
<ElButton v-if="dialogType === 'add'" @click="handleGenerateSeriesCode">
|
||||||
|
生成编码
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="系列名称" prop="series_name">
|
<ElFormItem label="系列名称" prop="series_name">
|
||||||
<ElInput v-model="form.series_name" placeholder="请输入系列名称" clearable />
|
<ElInput v-model="form.series_name" placeholder="请输入系列名称" clearable />
|
||||||
@@ -87,7 +93,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
import { PackageSeriesService } from '@/api/modules'
|
import { PackageSeriesService } from '@/api/modules'
|
||||||
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus'
|
import { ElMessage, ElMessageBox, ElTag, ElSwitch, ElButton } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import type { PackageSeriesResponse } from '@/types/api'
|
import type { PackageSeriesResponse } from '@/types/api'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
@@ -100,6 +106,7 @@
|
|||||||
frontendStatusToApi,
|
frontendStatusToApi,
|
||||||
apiStatusToFrontend
|
apiStatusToFrontend
|
||||||
} from '@/config/constants'
|
} from '@/config/constants'
|
||||||
|
import { generateSeriesCode } from '@/utils/codeGenerator'
|
||||||
|
|
||||||
defineOptions({ name: 'PackageSeries' })
|
defineOptions({ name: 'PackageSeries' })
|
||||||
|
|
||||||
@@ -338,6 +345,12 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成系列编码
|
||||||
|
const handleGenerateSeriesCode = () => {
|
||||||
|
form.series_code = generateSeriesCode()
|
||||||
|
ElMessage.success('编码生成成功')
|
||||||
|
}
|
||||||
|
|
||||||
// 删除套餐系列
|
// 删除套餐系列
|
||||||
const deleteSeries = (row: PackageSeriesResponse) => {
|
const deleteSeries = (row: PackageSeriesResponse) => {
|
||||||
ElMessageBox.confirm(`确定删除套餐系列 ${row.series_name} 吗?`, '删除确认', {
|
ElMessageBox.confirm(`确定删除套餐系列 ${row.series_name} 吗?`, '删除确认', {
|
||||||
|
|||||||
@@ -183,6 +183,115 @@
|
|||||||
|
|
||||||
<!-- 客户账号列表弹窗 -->
|
<!-- 客户账号列表弹窗 -->
|
||||||
<CustomerAccountDialog v-model="customerAccountDialogVisible" :shop-id="currentShopId" />
|
<CustomerAccountDialog v-model="customerAccountDialogVisible" :shop-id="currentShopId" />
|
||||||
|
|
||||||
|
<!-- 店铺操作右键菜单 -->
|
||||||
|
<ArtMenuRight
|
||||||
|
ref="shopOperationMenuRef"
|
||||||
|
:menu-items="shopOperationMenuItems"
|
||||||
|
:menu-width="140"
|
||||||
|
@select="handleShopOperationMenuSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 店铺默认角色管理对话框 -->
|
||||||
|
<ElDialog
|
||||||
|
v-model="defaultRolesDialogVisible"
|
||||||
|
:title="`设置默认角色 - ${currentShop?.shop_name || ''}`"
|
||||||
|
width="800px"
|
||||||
|
>
|
||||||
|
<div v-loading="defaultRolesLoading">
|
||||||
|
<!-- 当前默认角色列表 -->
|
||||||
|
<div class="default-roles-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span>当前默认角色</span>
|
||||||
|
<ElButton type="primary" @click="showAddRoleDialog">
|
||||||
|
添加角色
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElTable :data="currentDefaultRoles" border stripe style="margin-top: 12px">
|
||||||
|
<ElTableColumn prop="role_name" label="角色名称" width="150" />
|
||||||
|
<ElTableColumn prop="role_desc" label="角色描述" min-width="200" />
|
||||||
|
<ElTableColumn prop="status" label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag :type="row.status === CommonStatus.ENABLED ? 'success' : 'info'" size="small">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="操作" width="100" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElButton
|
||||||
|
type="danger"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
@click="handleDeleteDefaultRole(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<template #empty>
|
||||||
|
<div style="padding: 20px 0; color: #909399;">
|
||||||
|
暂无默认角色,请点击"添加角色"按钮进行配置
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="defaultRolesDialogVisible = false">关闭</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
|
||||||
|
<!-- 添加角色对话框 -->
|
||||||
|
<ElDialog
|
||||||
|
v-model="addRoleDialogVisible"
|
||||||
|
title="添加默认角色"
|
||||||
|
width="600px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div v-loading="rolesLoading">
|
||||||
|
<ElSelect
|
||||||
|
v-model="selectedRoleIds"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
placeholder="请选择要添加的角色"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="role in availableRoles"
|
||||||
|
:key="role.role_id"
|
||||||
|
:label="role.role_name"
|
||||||
|
:value="role.role_id"
|
||||||
|
:disabled="isRoleAlreadyAssigned(role.role_id)"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<span>{{ role.role_name }}</span>
|
||||||
|
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||||
|
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||||
|
</ElTag>
|
||||||
|
</div>
|
||||||
|
<span v-if="isRoleAlreadyAssigned(role.role_id)" style="color: #909399; font-size: 12px;">
|
||||||
|
已添加
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ElOption>
|
||||||
|
</ElSelect>
|
||||||
|
<div style="margin-top: 8px; color: #909399; font-size: 12px;">
|
||||||
|
支持多选,已添加的角色将显示为禁用状态
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<ElButton @click="addRoleDialogVisible = false">取消</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleAddDefaultRoles" :loading="addRoleLoading">
|
||||||
|
确定
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ArtTableFullScreen>
|
</ArtTableFullScreen>
|
||||||
@@ -202,10 +311,13 @@
|
|||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
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 ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||||
|
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||||
import CustomerAccountDialog from '@/components/business/CustomerAccountDialog.vue'
|
import CustomerAccountDialog from '@/components/business/CustomerAccountDialog.vue'
|
||||||
import { ShopService } from '@/api/modules'
|
import { ShopService, RoleService } from '@/api/modules'
|
||||||
import type { SearchFormItem } from '@/types'
|
import type { SearchFormItem } from '@/types'
|
||||||
import type { ShopResponse } from '@/types/api'
|
import type { ShopResponse, ShopRoleResponse } from '@/types/api'
|
||||||
|
import { RoleType } from '@/types/api'
|
||||||
import { formatDateTime } from '@/utils/business/format'
|
import { formatDateTime } from '@/utils/business/format'
|
||||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||||
|
|
||||||
@@ -221,6 +333,21 @@
|
|||||||
const parentShopList = ref<ShopResponse[]>([])
|
const parentShopList = ref<ShopResponse[]>([])
|
||||||
const searchParentShopList = ref<ShopResponse[]>([])
|
const searchParentShopList = ref<ShopResponse[]>([])
|
||||||
|
|
||||||
|
// 默认角色管理相关状态
|
||||||
|
const defaultRolesDialogVisible = ref(false)
|
||||||
|
const addRoleDialogVisible = ref(false)
|
||||||
|
const defaultRolesLoading = ref(false)
|
||||||
|
const rolesLoading = ref(false)
|
||||||
|
const addRoleLoading = ref(false)
|
||||||
|
const currentShop = ref<ShopResponse | null>(null)
|
||||||
|
const currentDefaultRoles = ref<ShopRoleResponse[]>([])
|
||||||
|
const availableRoles = ref<ShopRoleResponse[]>([])
|
||||||
|
const selectedRoleIds = ref<number[]>([])
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
const shopOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||||
|
const currentOperatingShop = ref<ShopResponse | null>(null)
|
||||||
|
|
||||||
// 定义表单搜索初始值
|
// 定义表单搜索初始值
|
||||||
const initialSearchState = {
|
const initialSearchState = {
|
||||||
shop_name: '',
|
shop_name: '',
|
||||||
@@ -476,7 +603,7 @@
|
|||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 220,
|
width: 200,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: (row: ShopResponse) => {
|
formatter: (row: ShopResponse) => {
|
||||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
@@ -485,12 +612,8 @@
|
|||||||
onClick: () => viewCustomerAccounts(row)
|
onClick: () => viewCustomerAccounts(row)
|
||||||
}),
|
}),
|
||||||
h(ArtButtonTable, {
|
h(ArtButtonTable, {
|
||||||
type: 'edit',
|
text: '更多操作',
|
||||||
onClick: () => showDialog('edit', row)
|
onContextmenu: (e: MouseEvent) => showShopOperationMenu(e, row)
|
||||||
}),
|
|
||||||
h(ArtButtonTable, {
|
|
||||||
type: 'delete',
|
|
||||||
onClick: () => deleteShop(row)
|
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -750,6 +873,47 @@
|
|||||||
customerAccountDialogVisible.value = true
|
customerAccountDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 店铺操作菜单项配置
|
||||||
|
const shopOperationMenuItems = computed((): MenuItemType[] => [
|
||||||
|
{
|
||||||
|
key: 'defaultRoles',
|
||||||
|
label: '默认角色'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 显示店铺操作右键菜单
|
||||||
|
const showShopOperationMenu = (e: MouseEvent, row: ShopResponse) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
currentOperatingShop.value = row
|
||||||
|
shopOperationMenuRef.value?.show(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理店铺操作菜单选择
|
||||||
|
const handleShopOperationMenuSelect = (item: MenuItemType) => {
|
||||||
|
if (!currentOperatingShop.value) return
|
||||||
|
|
||||||
|
switch (item.key) {
|
||||||
|
case 'defaultRoles':
|
||||||
|
showDefaultRolesDialog(currentOperatingShop.value)
|
||||||
|
break
|
||||||
|
case 'edit':
|
||||||
|
showDialog('edit', currentOperatingShop.value)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
deleteShop(currentOperatingShop.value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 状态切换
|
// 状态切换
|
||||||
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
|
const handleStatusChange = async (row: ShopResponse, newStatus: number) => {
|
||||||
const oldStatus = row.status
|
const oldStatus = row.status
|
||||||
@@ -767,10 +931,144 @@
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 默认角色管理功能 ==========
|
||||||
|
|
||||||
|
// 显示默认角色管理对话框
|
||||||
|
const showDefaultRolesDialog = async (row: ShopResponse) => {
|
||||||
|
currentShop.value = row
|
||||||
|
defaultRolesDialogVisible.value = true
|
||||||
|
await loadShopDefaultRoles(row.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载店铺默认角色列表
|
||||||
|
const loadShopDefaultRoles = async (shopId: number) => {
|
||||||
|
defaultRolesLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await ShopService.getShopRoles(shopId)
|
||||||
|
if (res.code === 0) {
|
||||||
|
currentDefaultRoles.value = res.data.roles || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取店铺默认角色失败:', error)
|
||||||
|
ElMessage.error('获取店铺默认角色失败')
|
||||||
|
} finally {
|
||||||
|
defaultRolesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示添加角色对话框
|
||||||
|
const showAddRoleDialog = async () => {
|
||||||
|
addRoleDialogVisible.value = true
|
||||||
|
selectedRoleIds.value = []
|
||||||
|
await loadAvailableRoles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载可用角色列表
|
||||||
|
const loadAvailableRoles = async () => {
|
||||||
|
rolesLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await RoleService.getRoles({
|
||||||
|
page: 1,
|
||||||
|
page_size: 9999,
|
||||||
|
status: 1 // RoleStatus.ENABLED
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
// 将 PlatformRole 转换为与 ShopRoleResponse 兼容的格式,同时保留 role_type
|
||||||
|
availableRoles.value = (res.data.items || []).map((role) => ({
|
||||||
|
...role,
|
||||||
|
role_id: role.ID,
|
||||||
|
role_name: role.role_name,
|
||||||
|
role_desc: role.role_desc,
|
||||||
|
role_type: role.role_type, // 保留角色类型用于显示
|
||||||
|
shop_id: 0 // 这个值在可用角色列表中不使用
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取角色列表失败:', error)
|
||||||
|
ElMessage.error('获取角色列表失败')
|
||||||
|
} finally {
|
||||||
|
rolesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断角色是否已分配
|
||||||
|
const isRoleAlreadyAssigned = (roleId: number) => {
|
||||||
|
return currentDefaultRoles.value.some((r) => r.role_id === roleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加默认角色
|
||||||
|
const handleAddDefaultRoles = async () => {
|
||||||
|
if (selectedRoleIds.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一个角色')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentShop.value) {
|
||||||
|
ElMessage.error('店铺信息异常')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addRoleLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await ShopService.assignShopRoles(currentShop.value.id, {
|
||||||
|
role_ids: selectedRoleIds.value
|
||||||
|
})
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success('添加默认角色成功')
|
||||||
|
addRoleDialogVisible.value = false
|
||||||
|
// 刷新默认角色列表
|
||||||
|
await loadShopDefaultRoles(currentShop.value.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加默认角色失败:', error)
|
||||||
|
} finally {
|
||||||
|
addRoleLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除默认角色
|
||||||
|
const handleDeleteDefaultRole = (role: ShopRoleResponse) => {
|
||||||
|
ElMessageBox.confirm(`确定要删除默认角色 "${role.role_name}" 吗?`, '删除默认角色', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
if (!currentShop.value) {
|
||||||
|
ElMessage.error('店铺信息异常')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ShopService.deleteShopRole(currentShop.value.id, role.role_id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
// 刷新默认角色列表
|
||||||
|
await loadShopDefaultRoles(currentShop.value.id)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除默认角色失败:', error)
|
||||||
|
ElMessage.error('删除默认角色失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 用户取消删除
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.shop-page {
|
.shop-page {
|
||||||
// 店铺管理页面样式
|
// 店铺管理页面样式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.default-roles-section {
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user