modify
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m13s

This commit is contained in:
sexygoat
2026-04-02 13:42:57 +08:00
parent 411206e039
commit 8df7024a45
83 changed files with 1830 additions and 1865 deletions

View File

@@ -35,20 +35,24 @@
根据提供的权限JSON需要添加权限控制的模块包括 根据提供的权限JSON需要添加权限控制的模块包括
1. **套餐管理** (`/package-management`) 1. **套餐管理** (`/package-management`)
- 套餐系列 (`package-series`) - 套餐系列 (`package-series`)
- 套餐列表 (`package-list`) - 套餐列表 (`package-list`)
- 单套餐分配 (`package-assign`) - 单套餐分配 (`package-assign`)
- 套餐系列分配 (`series-assign`) - 套餐系列分配 (`series-assign`)
2. **店铺管理** (`/shop-management`) 2. **店铺管理** (`/shop-management`)
- 店铺列表 (`list`) - 店铺列表 (`list`)
3. **账号管理** (`/account-management`) 3. **账号管理** (`/account-management`)
- 账号列表 (`account`) - 账号列表 (`account`)
- 平台账号 (`platform-account`) - 平台账号 (`platform-account`)
- 代理账号 (`shop-account`) - 代理账号 (`shop-account`)
4. **资产管理** (`/asset-management`) 4. **资产管理** (`/asset-management`)
- IoT卡管理 (`iot-card-management`) - IoT卡管理 (`iot-card-management`)
- IoT卡任务 (`iot-card-task`) - IoT卡任务 (`iot-card-task`)
- 设备任务 (`device-task`) - 设备任务 (`device-task`)
@@ -67,9 +71,9 @@
```vue ```vue
<script setup> <script setup>
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
</script> </script>
<template> <template>
@@ -79,29 +83,28 @@ const { hasAuth } = useAuth()
</template> </template>
<!-- 2. 表格列中的状态切换 --> <!-- 2. 表格列中的状态切换 -->
<ElSwitch <ElSwitch :disabled="!hasAuth('module:update_status')" v-model="row.status" />
:disabled="!hasAuth('module:update_status')"
v-model="row.status"
/>
<!-- 3. 表格列中的操作按钮 --> <!-- 3. 表格列中的操作按钮 -->
<script> <script>
const columns = [{ const columns = [
prop: 'operation', {
formatter: (row) => { prop: 'operation',
const buttons = [] formatter: (row) => {
const buttons = []
if (hasAuth('module:edit')) { if (hasAuth('module:edit')) {
buttons.push(h(ArtButtonTable, { type: 'edit', onClick: () => edit(row) })) buttons.push(h(ArtButtonTable, { type: 'edit', onClick: () => edit(row) }))
}
if (hasAuth('module:delete')) {
buttons.push(h(ArtButtonTable, { type: 'delete', onClick: () => del(row) }))
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
} }
]
if (hasAuth('module:delete')) {
buttons.push(h(ArtButtonTable, { type: 'delete', onClick: () => del(row) }))
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}]
</script> </script>
</template> </template>
``` ```

View File

@@ -9,12 +9,14 @@
### 1. 套餐管理模块 (Package Management) ✅ ### 1. 套餐管理模块 (Package Management) ✅
- [x] **套餐系列页面** (`/package-management/package-series`) - [x] **套餐系列页面** (`/package-management/package-series`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增套餐系列"按钮添加 `v-permission="'package_series:add'"` - ✅ 为"新增套餐系列"按钮添加 `v-permission="'package_series:add'"`
- ✅ 为状态切换添加 `disabled: !hasAuth('package_series:update_status')` - ✅ 为状态切换添加 `disabled: !hasAuth('package_series:update_status')`
- ✅ 在操作列中使用 `hasAuth()` 判断编辑权限 (`package_series:edit`)和删除权限 (`package_series:delete`) - ✅ 在操作列中使用 `hasAuth()` 判断编辑权限 (`package_series:edit`)和删除权限 (`package_series:delete`)
- [x] **套餐列表页面** (`/package-management/package-list`) - [x] **套餐列表页面** (`/package-management/package-list`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增套餐"按钮添加 `v-permission="'package:add'"` - ✅ 为"新增套餐"按钮添加 `v-permission="'package:add'"`
- ✅ 为上/下架切换添加 `disabled: !hasAuth('package:update_away')` - ✅ 为上/下架切换添加 `disabled: !hasAuth('package:update_away')`
@@ -22,6 +24,7 @@
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`package:edit`) 和删除 (`package:delete`) 权限 - ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`package:edit`) 和删除 (`package:delete`) 权限
- [x] **单套餐分配页面** (`/package-management/package-assign`) - [x] **单套餐分配页面** (`/package-management/package-assign`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增分配"按钮添加 `v-permission="'package_assign:add'"` - ✅ 为"新增分配"按钮添加 `v-permission="'package_assign:add'"`
- ✅ 为状态切换添加 `disabled: !hasAuth('package_assign:update_status')` - ✅ 为状态切换添加 `disabled: !hasAuth('package_assign:update_status')`
@@ -50,6 +53,7 @@
### 3. 账号管理模块 (Account Management) ✅ ### 3. 账号管理模块 (Account Management) ✅
- [x] **账号列表页面** (`/account-management/account`) - [x] **账号列表页面** (`/account-management/account`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增账号"按钮添加 `v-permission="'account:add'"` - ✅ 为"新增账号"按钮添加 `v-permission="'account:add'"`
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限: - ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
@@ -58,6 +62,7 @@
- 删除: `account:delete` - 删除: `account:delete`
- [x] **平台账号页面** (`/account-management/platform-account`) - [x] **平台账号页面** (`/account-management/platform-account`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增平台账号"按钮添加 `v-permission="'platform_account:add'"` - ✅ 为"新增平台账号"按钮添加 `v-permission="'platform_account:add'"`
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限: - ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
@@ -76,6 +81,7 @@
### 4. 资产管理模块 (Asset Management) ✅ ### 4. 资产管理模块 (Asset Management) ✅
- [x] **IoT卡管理页面** (`/asset-management/iot-card-management`) - [x] **IoT卡管理页面** (`/asset-management/iot-card-management`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为表格头部操作按钮添加权限控制: - ✅ 为表格头部操作按钮添加权限控制:
- 批量分配: `v-permission="'iot_card:batch_allocation'"` - 批量分配: `v-permission="'iot_card:batch_allocation'"`
@@ -89,16 +95,19 @@
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **IoT卡任务页面** (`/asset-management/iot-card-task`) - [x] **IoT卡任务页面** (`/asset-management/iot-card-task`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"批量导入IoT卡"按钮添加 `v-permission="''"` - ✅ 为"批量导入IoT卡"按钮添加 `v-permission="''"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **设备任务页面** (`/asset-management/device-task`) - [x] **设备任务页面** (`/asset-management/device-task`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"批量导入设备"按钮添加 `v-permission="'device_task:bulk_import'"` - ✅ 为"批量导入设备"按钮添加 `v-permission="'device_task:bulk_import'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **设备管理页面** (`/asset-management/device-list`) - [x] **设备管理页面** (`/asset-management/device-list`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为表格头部操作按钮添加权限控制: - ✅ 为表格头部操作按钮添加权限控制:
- 批量分配: `v-permission="'devices:batch_allocation'"` - 批量分配: `v-permission="'devices:batch_allocation'"`
@@ -117,6 +126,7 @@
### 5. 账户管理模块 (Account Module) ✅ ### 5. 账户管理模块 (Account Module) ✅
- [x] **企业客户页面** (`/account/enterprise-customer`) - [x] **企业客户页面** (`/account/enterprise-customer`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增企业客户"按钮添加 `v-permission="'enterprise_customer:add'"` - ✅ 为"新增企业客户"按钮添加 `v-permission="'enterprise_customer:add'"`
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限: - ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
@@ -127,6 +137,7 @@
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **运营商管理页面** (`/account/carrier-management`) - [x] **运营商管理页面** (`/account/carrier-management`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增运营商"按钮添加 `v-permission="'carrier:add'"` - ✅ 为"新增运营商"按钮添加 `v-permission="'carrier:add'"`
- ✅ 为状态切换添加 `disabled: !hasAuth('carrier:update_status')` - ✅ 为状态切换添加 `disabled: !hasAuth('carrier:update_status')`
@@ -134,16 +145,19 @@
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **订单管理页面** (`/account/orders`) - [x] **订单管理页面** (`/account/orders`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"创建订单"按钮添加 `v-permission="'orders:add'"` - ✅ 为"创建订单"按钮添加 `v-permission="'orders:add'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **提现配置页面** (`/account/commission/withdrawal-settings`) - [x] **提现配置页面** (`/account/commission/withdrawal-settings`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"新增配置"按钮添加 `v-permission="'withdrawal_settings:add'"` - ✅ 为"新增配置"按钮添加 `v-permission="'withdrawal_settings:add'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
- [x] **我的佣金页面** (`/account/commission/my-commission`) - [x] **我的佣金页面** (`/account/commission/my-commission`)
- ✅ 引入 `useAuth` composable - ✅ 引入 `useAuth` composable
- ✅ 为"发起提现"按钮添加 `v-permission="'my_commission:add'"` - ✅ 为"发起提现"按钮添加 `v-permission="'my_commission:add'"`
- ✅ 验证权限控制正确工作 - ✅ 验证权限控制正确工作
@@ -164,6 +178,7 @@
### 7. 测试与文档 ### 7. 测试与文档
- [ ] **权限控制测试** - [ ] **权限控制测试**
- 创建不同权限组合的测试用户 - 创建不同权限组合的测试用户
- 验证每个页面的按钮显示/隐藏符合预期 - 验证每个页面的按钮显示/隐藏符合预期
- 验证状态切换的启用/禁用符合预期 - 验证状态切换的启用/禁用符合预期

View File

@@ -7,6 +7,7 @@
## What Changes ## What Changes
### IoT Card Operations (6 new APIs) ### IoT Card Operations (6 new APIs)
- 查询流量使用情况 (GET /api/admin/iot-cards/{iccid}/gateway-flow) - 查询流量使用情况 (GET /api/admin/iot-cards/{iccid}/gateway-flow)
- 查询实名认证状态 (GET /api/admin/iot-cards/{iccid}/gateway-realname) - 查询实名认证状态 (GET /api/admin/iot-cards/{iccid}/gateway-realname)
- 查询卡片状态 (GET /api/admin/iot-cards/{iccid}/gateway-status) - 查询卡片状态 (GET /api/admin/iot-cards/{iccid}/gateway-status)
@@ -15,6 +16,7 @@
- 停用物联网卡 (POST /api/admin/iot-cards/{iccid}/stop) - 停用物联网卡 (POST /api/admin/iot-cards/{iccid}/stop)
### Device Operations (6 new APIs) ### Device Operations (6 new APIs)
- 重启设备 (POST /api/admin/devices/by-imei/{imei}/reboot) - 重启设备 (POST /api/admin/devices/by-imei/{imei}/reboot)
- 重置设备 (POST /api/admin/devices/by-imei/{imei}/reset) - 重置设备 (POST /api/admin/devices/by-imei/{imei}/reset)
- 设置限速 (PUT /api/admin/devices/by-imei/{imei}/speed-limit) - 设置限速 (PUT /api/admin/devices/by-imei/{imei}/speed-limit)
@@ -22,6 +24,7 @@
- 设置WiFi (PUT /api/admin/devices/by-imei/{imei}/wifi) - 设置WiFi (PUT /api/admin/devices/by-imei/{imei}/wifi)
### UI Changes ### UI Changes
-`/asset-management/iot-card-management` 页面添加操作按钮: -`/asset-management/iot-card-management` 页面添加操作按钮:
- "查询流量"按钮(主要操作) - "查询流量"按钮(主要操作)
- "更多操作"下拉菜单包含其他5个操作 - "更多操作"下拉菜单包含其他5个操作
@@ -32,10 +35,12 @@
## Impact ## Impact
### Affected Specs ### Affected Specs
- **NEW**: `specs/iot-card-operations/spec.md` - IoT卡操作相关的所有需求 - **NEW**: `specs/iot-card-operations/spec.md` - IoT卡操作相关的所有需求
- **NEW**: `specs/device-operations/spec.md` - 设备操作相关的所有需求 - **NEW**: `specs/device-operations/spec.md` - 设备操作相关的所有需求
### Affected Code ### Affected Code
- **API层**: - **API层**:
- 新增 `src/api/modules/iotCard.ts` - IoT卡操作API方法 - 新增 `src/api/modules/iotCard.ts` - IoT卡操作API方法
- 新增 `src/api/modules/device.ts` - 设备操作API方法 - 新增 `src/api/modules/device.ts` - 设备操作API方法
@@ -47,6 +52,7 @@
- 修改 `src/views/asset-management/devices/index.vue` - 添加操作按钮和对话框 - 修改 `src/views/asset-management/devices/index.vue` - 添加操作按钮和对话框
### Breaking Changes ### Breaking Changes
无破坏性变更。所有变更都是增量式的新功能添加。 无破坏性变更。所有变更都是增量式的新功能添加。
## Dependencies ## Dependencies

View File

@@ -9,9 +9,11 @@
**API端点**: `GET /api/admin/shops/{shop_id}/roles` **API端点**: `GET /api/admin/shops/{shop_id}/roles`
**路径参数**: **路径参数**:
- `shop_id` (integer, required): 店铺ID - `shop_id` (integer, required): 店铺ID
**响应数据**: **响应数据**:
```typescript ```typescript
{ {
shop_id: number // 店铺ID shop_id: number // 店铺ID
@@ -54,9 +56,11 @@ interface ShopRole {
**API端点**: `POST /api/admin/shops/{shop_id}/roles` **API端点**: `POST /api/admin/shops/{shop_id}/roles`
**路径参数**: **路径参数**:
- `shop_id` (integer, required): 店铺ID - `shop_id` (integer, required): 店铺ID
**请求体**: **请求体**:
```typescript ```typescript
{ {
role_ids: number[] | null // 角色ID列表 role_ids: number[] | null // 角色ID列表
@@ -106,6 +110,7 @@ interface ShopRole {
**API端点**: `DELETE /api/admin/shops/{shop_id}/roles/{role_id}` **API端点**: `DELETE /api/admin/shops/{shop_id}/roles/{role_id}`
**路径参数**: **路径参数**:
- `shop_id` (integer, required): 店铺ID - `shop_id` (integer, required): 店铺ID
- `role_id` (integer, required): 角色ID - `role_id` (integer, required): 角色ID

View File

@@ -36,11 +36,13 @@
### 完成的功能 ### 完成的功能
1. **类型定义** - 已在 `src/types/api/shop.ts` 添加了三个接口: 1. **类型定义** - 已在 `src/types/api/shop.ts` 添加了三个接口:
- `ShopRoleResponse`: 店铺角色响应实体 - `ShopRoleResponse`: 店铺角色响应实体
- `ShopRolesResponse`: 店铺角色列表响应 - `ShopRolesResponse`: 店铺角色列表响应
- `AssignShopRolesRequest`: 分配角色请求 - `AssignShopRolesRequest`: 分配角色请求
2. **API 服务层** - 已在 `src/api/modules/shop.ts``ShopService` 类中添加: 2. **API 服务层** - 已在 `src/api/modules/shop.ts``ShopService` 类中添加:
- `getShopRoles(shopId)`: 获取店铺默认角色列表 - `getShopRoles(shopId)`: 获取店铺默认角色列表
- `assignShopRoles(shopId, data)`: 分配店铺默认角色 - `assignShopRoles(shopId, data)`: 分配店铺默认角色
- `deleteShopRole(shopId, roleId)`: 删除店铺默认角色 - `deleteShopRole(shopId, roleId)`: 删除店铺默认角色
@@ -56,6 +58,7 @@
### 待测试项 ### 待测试项
所有开发任务已完成,现在需要进行功能测试以确保: 所有开发任务已完成,现在需要进行功能测试以确保:
- API 调用正常工作 - API 调用正常工作
- UI 交互符合预期 - UI 交互符合预期
- 边界情况处理正确 - 边界情况处理正确

View File

@@ -7,6 +7,7 @@
## What Changes ## What Changes
- **BREAKING**: 重命名 4 个 API 端点的请求/响应字段 - **BREAKING**: 重命名 4 个 API 端点的请求/响应字段
- `PATCH /api/admin/iot-cards/series-binding` - 请求参数 `series_allocation_id``series_id` - `PATCH /api/admin/iot-cards/series-binding` - 请求参数 `series_allocation_id``series_id`
- `PATCH /api/admin/devices/series-binding` - 请求参数 `series_allocation_id``series_id` - `PATCH /api/admin/devices/series-binding` - 请求参数 `series_allocation_id``series_id`
- `GET /api/admin/iot-cards/standalone` - 查询参数和响应字段 `series_allocation_id``series_id` - `GET /api/admin/iot-cards/standalone` - 查询参数和响应字段 `series_allocation_id``series_id`

View File

@@ -25,6 +25,7 @@ The system SHALL support filtering devices by package series ID in query paramet
The system SHALL include package series ID in device response data. The system SHALL include package series ID in device response data.
**Response Field**: `series_id` (number | null) **Response Field**: `series_id` (number | null)
- Value is package series ID if device is bound to a series - Value is package series ID if device is bound to a series
- Value is null if device has no series binding - Value is null if device has no series binding
@@ -58,18 +59,21 @@ The system SHALL provide an endpoint to batch set package series binding for dev
**Endpoint**: `PATCH /api/admin/devices/series-binding` **Endpoint**: `PATCH /api/admin/devices/series-binding`
**Request Body**: **Request Body**:
```json ```json
{ {
"device_ids": [1, 2, 3], "device_ids": [1, 2, 3],
"series_id": 123 // Changed from series_allocation_id "series_id": 123 // Changed from series_allocation_id
} }
``` ```
**Field Definitions**: **Field Definitions**:
- `device_ids`: Array of device IDs (max 500 items) - `device_ids`: Array of device IDs (max 500 items)
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association - `series_id`: Package series ID (tb_package_series.id), 0 means clear association
**Response**: **Response**:
```json ```json
{ {
"code": 0, "code": 0,

View File

@@ -25,6 +25,7 @@ The system SHALL support filtering standalone IoT cards by package series ID in
The system SHALL include package series ID in standalone IoT card response data. The system SHALL include package series ID in standalone IoT card response data.
**Response Field**: `series_id` (number | null) **Response Field**: `series_id` (number | null)
- Value is package series ID if card is bound to a series - Value is package series ID if card is bound to a series
- Value is null if card has no series binding - Value is null if card has no series binding
@@ -58,18 +59,21 @@ The system SHALL provide an endpoint to batch set package series binding for IoT
**Endpoint**: `PATCH /api/admin/iot-cards/series-binding` **Endpoint**: `PATCH /api/admin/iot-cards/series-binding`
**Request Body**: **Request Body**:
```json ```json
{ {
"iccids": ["898600..."], "iccids": ["898600..."],
"series_id": 123 // Changed from series_allocation_id "series_id": 123 // Changed from series_allocation_id
} }
``` ```
**Field Definitions**: **Field Definitions**:
- `iccids`: Array of ICCIDs (max 500 items) - `iccids`: Array of ICCIDs (max 500 items)
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association - `series_id`: Package series ID (tb_package_series.id), 0 means clear association
**Response**: **Response**:
```json ```json
{ {
"code": 0, "code": 0,

View File

@@ -3,14 +3,17 @@
## 1. 类型定义更新 ## 1. 类型定义更新
### 1.1 批量设置接口参数重命名 ### 1.1 批量设置接口参数重命名
- [x] 1.1.1 更新 `src/types/api/device.ts` 中 BatchSetDeviceSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id` - [x] 1.1.1 更新 `src/types/api/device.ts` 中 BatchSetDeviceSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
- [x] 1.1.2 更新 `src/types/api/card.ts` 中 BatchSetCardSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id` - [x] 1.1.2 更新 `src/types/api/card.ts` 中 BatchSetCardSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
### 1.2 查询参数新增字段 ### 1.2 查询参数新增字段
- [x] 1.2.1 在 `src/types/api/device.ts` 的 DeviceQueryParams 接口中添加 `series_id?: number` 查询参数 - [x] 1.2.1 在 `src/types/api/device.ts` 的 DeviceQueryParams 接口中添加 `series_id?: number` 查询参数
- [x] 1.2.2 在 `src/types/api/card.ts` 的 StandaloneCardQueryParams 接口中添加 `series_id?: number` 查询参数 - [x] 1.2.2 在 `src/types/api/card.ts` 的 StandaloneCardQueryParams 接口中添加 `series_id?: number` 查询参数
### 1.3 响应类型新增字段 ### 1.3 响应类型新增字段
- [x] 1.3.1 在 `src/types/api/device.ts` 的 Device 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面) - [x] 1.3.1 在 `src/types/api/device.ts` 的 Device 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
- [x] 1.3.2 在 `src/types/api/card.ts` 的 StandaloneIotCard 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面) - [x] 1.3.2 在 `src/types/api/card.ts` 的 StandaloneIotCard 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)

View File

@@ -17,6 +17,7 @@
**修改**: `src/types/api/packageManagement.ts` **修改**: `src/types/api/packageManagement.ts`
**套餐系列相关类型**: **套餐系列相关类型**:
- `PackageSeriesResponse` - 新增 `enable_one_time_commission``one_time_commission_config` - `PackageSeriesResponse` - 新增 `enable_one_time_commission``one_time_commission_config`
- `CreatePackageSeriesRequest` - 新增一次性佣金配置字段 - `CreatePackageSeriesRequest` - 新增一次性佣金配置字段
- `UpdatePackageSeriesRequest` - 新增一次性佣金配置字段 - `UpdatePackageSeriesRequest` - 新增一次性佣金配置字段
@@ -24,6 +25,7 @@
- 新增 `OneTimeCommissionTier` - 梯度佣金档位配置 - 新增 `OneTimeCommissionTier` - 梯度佣金档位配置
**系列分配相关类型**: **系列分配相关类型**:
- **BREAKING**: `ShopSeriesAllocationResponse` - 完全重构字段结构 - **BREAKING**: `ShopSeriesAllocationResponse` - 完全重构字段结构
- 移除: `base_commission` (BaseCommissionConfig) - 移除: `base_commission` (BaseCommissionConfig)
- 新增: `series_code`, `enable_force_recharge`, `enable_one_time_commission`, `force_recharge_amount`, `force_recharge_trigger_type`, `one_time_commission_amount`, `one_time_commission_threshold`, `one_time_commission_trigger` - 新增: `series_code`, `enable_force_recharge`, `enable_one_time_commission`, `force_recharge_amount`, `force_recharge_trigger_type`, `one_time_commission_amount`, `one_time_commission_threshold`, `one_time_commission_trigger`
@@ -32,6 +34,7 @@
- 移除不再使用的类型: `BaseCommissionConfig`, `TierEntry`, `TierCommissionConfig`, `OneTimeCommissionTierEntry`, `OneTimeCommissionConfig`(旧版) - 移除不再使用的类型: `BaseCommissionConfig`, `TierEntry`, `TierCommissionConfig`, `OneTimeCommissionTierEntry`, `OneTimeCommissionConfig`(旧版)
**单套餐分配相关类型**: **单套餐分配相关类型**:
- `ShopPackageAllocationResponse` - 字段调整 - `ShopPackageAllocationResponse` - 字段调整
- 新增: `series_id`, `series_name`, `series_allocation_id`, `allocator_shop_id`, `allocator_shop_name` - 新增: `series_id`, `series_name`, `series_allocation_id`, `allocator_shop_id`, `allocator_shop_name`
- 移除: `allocation_id` (重命名为 `series_allocation_id`), `calculated_cost_price` - 移除: `allocation_id` (重命名为 `series_allocation_id`), `calculated_cost_price`
@@ -44,6 +47,7 @@
### 3. 页面/组件适配(实施阶段处理) ### 3. 页面/组件适配(实施阶段处理)
以下文件需要适配新的数据结构(本提案阶段不编写代码): 以下文件需要适配新的数据结构(本提案阶段不编写代码):
- 套餐系列管理页面 - 需支持一次性佣金配置表单 - 套餐系列管理页面 - 需支持一次性佣金配置表单
- 系列分配页面 - 需重构佣金配置表单UI - 系列分配页面 - 需重构佣金配置表单UI
- 单套餐分配页面 - 需展示新增的关联字段 - 单套餐分配页面 - 需展示新增的关联字段

View File

@@ -7,6 +7,7 @@
套餐系列的数据模型SHALL支持一次性佣金配置包括固定佣金、梯度佣金、强制充值、时效类型等高级功能。 套餐系列的数据模型SHALL支持一次性佣金配置包括固定佣金、梯度佣金、强制充值、时效类型等高级功能。
**字段定义**: **字段定义**:
- `id` (number) - 系列ID - `id` (number) - 系列ID
- `series_code` (string) - 系列编码,唯一标识 - `series_code` (string) - 系列编码,唯一标识
- `series_name` (string) - 系列名称 - `series_name` (string) - 系列名称
@@ -18,6 +19,7 @@
- `updated_at` (string) - 更新时间 - `updated_at` (string) - 更新时间
**一次性佣金配置结构** (`SeriesOneTimeCommissionConfig`): **一次性佣金配置结构** (`SeriesOneTimeCommissionConfig`):
- `enable` (boolean) - 是否启用一次性佣金 - `enable` (boolean) - 是否启用一次性佣金
- `commission_type` (string) - 佣金类型: "fixed"(固定) | "tiered"(梯度) - `commission_type` (string) - 佣金类型: "fixed"(固定) | "tiered"(梯度)
- `commission_amount` (number, optional) - 固定佣金金额commission_type=fixed时使用 - `commission_amount` (number, optional) - 固定佣金金额commission_type=fixed时使用
@@ -31,6 +33,7 @@
- `validity_value` (string, optional) - 时效值(日期字符串或月数) - `validity_value` (string, optional) - 时效值(日期字符串或月数)
**梯度档位结构** (`OneTimeCommissionTier`): **梯度档位结构** (`OneTimeCommissionTier`):
- `threshold` (number) - 达标阈值 - `threshold` (number) - 达标阈值
- `dimension` (string) - 统计维度: "sales_count"(销量) | "sales_amount"(销售额) - `dimension` (string) - 统计维度: "sales_count"(销量) | "sales_amount"(销售额)
- `amount` (number) - 佣金金额(分) - `amount` (number) - 佣金金额(分)
@@ -64,10 +67,7 @@
- `commission_type`: "tiered" - `commission_type`: "tiered"
- `threshold`: 5000 - `threshold`: 5000
- `trigger_type`: "accumulated_recharge" - `trigger_type`: "accumulated_recharge"
- `tiers`: [ - `tiers`: [ { threshold: 10, dimension: "sales_count", amount: 3000 }, { threshold: 50, dimension: "sales_count", amount: 8000 } ]
{ threshold: 10, dimension: "sales_count", amount: 3000 },
{ threshold: 50, dimension: "sales_count", amount: 8000 }
]
- `validity_type`: "relative" - `validity_type`: "relative"
- `validity_value`: "12" (12个月) - `validity_value`: "12" (12个月)
- **THEN** 系统SHALL创建套餐系列并保存梯度佣金配置 - **THEN** 系统SHALL创建套餐系列并保存梯度佣金配置

View File

@@ -7,6 +7,7 @@
单套餐分配的数据模型SHALL新增系列关联字段和分配者信息字段以支持更完整的分配关系追溯。 单套餐分配的数据模型SHALL新增系列关联字段和分配者信息字段以支持更完整的分配关系追溯。
**字段定义**: **字段定义**:
- `id` (number) - 分配ID - `id` (number) - 分配ID
- `package_id` (number) - 套餐ID - `package_id` (number) - 套餐ID
- `package_code` (string) - 套餐编码 - `package_code` (string) - 套餐编码
@@ -24,6 +25,7 @@
- `updated_at` (string) - 更新时间 - `updated_at` (string) - 更新时间
**查询参数** (`ShopPackageAllocationQueryParams`): **查询参数** (`ShopPackageAllocationQueryParams`):
- `page` (number, optional) - 页码 - `page` (number, optional) - 页码
- `page_size` (number, optional) - 每页数量 - `page_size` (number, optional) - 每页数量
- `shop_id` (number, optional) - 被分配的店铺ID - `shop_id` (number, optional) - 被分配的店铺ID

View File

@@ -7,6 +7,7 @@
套餐系列分配的数据模型SHALL重构为更细粒度的佣金配置结构移除旧的 `base_commission` 模式,采用独立的一次性佣金和强制充值配置字段。 套餐系列分配的数据模型SHALL重构为更细粒度的佣金配置结构移除旧的 `base_commission` 模式,采用独立的一次性佣金和强制充值配置字段。
**字段定义**: **字段定义**:
- `id` (number) - 分配ID - `id` (number) - 分配ID
- `series_id` (number) - 套餐系列ID - `series_id` (number) - 套餐系列ID
- `series_name` (string) - 套餐系列名称 - `series_name` (string) - 套餐系列名称
@@ -27,6 +28,7 @@
- `updated_at` (string) - 更新时间 - `updated_at` (string) - 更新时间
**创建请求字段** (`CreateShopSeriesAllocationRequest`): **创建请求字段** (`CreateShopSeriesAllocationRequest`):
- `series_id` (number, required) - 套餐系列ID - `series_id` (number, required) - 套餐系列ID
- `shop_id` (number, required) - 被分配的店铺ID - `shop_id` (number, required) - 被分配的店铺ID
- `one_time_commission_amount` (number, required) - 一次性佣金金额上限(分) - `one_time_commission_amount` (number, required) - 一次性佣金金额上限(分)
@@ -38,6 +40,7 @@
- `force_recharge_trigger_type` (number, optional) - 强充触发类型 - `force_recharge_trigger_type` (number, optional) - 强充触发类型
**更新请求字段** (`UpdateShopSeriesAllocationRequest`): **更新请求字段** (`UpdateShopSeriesAllocationRequest`):
- 所有字段均为可选,允许部分更新 - 所有字段均为可选,允许部分更新
- `enable_one_time_commission` (boolean, optional) - `enable_one_time_commission` (boolean, optional)
- `one_time_commission_amount` (number, optional) - `one_time_commission_amount` (number, optional)

View File

@@ -20,10 +20,7 @@ export class AgentRechargeService extends BaseService {
static getAgentRecharges( static getAgentRecharges(
params?: AgentRechargeQueryParams params?: AgentRechargeQueryParams
): Promise<BaseResponse<AgentRechargeListResponse>> { ): Promise<BaseResponse<AgentRechargeListResponse>> {
return this.get<BaseResponse<AgentRechargeListResponse>>( return this.get<BaseResponse<AgentRechargeListResponse>>('/api/admin/agent-recharges', params)
'/api/admin/agent-recharges',
params
)
} }
/** /**

View File

@@ -383,19 +383,19 @@ export class CardService extends BaseService {
} }
/** /**
* 启用 IoT 卡 * 启用 IoT 卡 单卡复机
* @param id IoT卡ID * @param iccid
*/ */
static enableIotCard(id: number): Promise<BaseResponse> { static enableIotCard(iccid: string): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/iot-cards/${id}/enable`, {}) return this.post<BaseResponse>(`/api/admin/assets/card/${iccid}/start`, {})
} }
/** /**
* 停用 IoT 卡 * 停用 IoT 卡 单卡停机
* @param id IoT卡ID * @param iccid
*/ */
static disableIotCard(id: number): Promise<BaseResponse> { static disableIotCard(iccid: string): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/iot-cards/${id}/disable`, {}) return this.post<BaseResponse>(`/api/admin/assets/card/${iccid}/stop`, {})
} }
/** /**
@@ -410,17 +410,8 @@ export class CardService extends BaseService {
* 获取套餐流量详单(每日流量记录) * 获取套餐流量详单(每日流量记录)
* @param packageUsageId 套餐使用记录ID * @param packageUsageId 套餐使用记录ID
*/ */
static getPackageDailyRecords(packageUsageId: number): Promise<BaseResponse<{ static getPackageDailyRecords(packageUsageId: number): Promise<
package_name: string BaseResponse<{
package_usage_id: number
total_usage_mb: number
records: Array<{
date: string
daily_usage_mb: number
cumulative_usage_mb: number
}>
}>> {
return this.get<BaseResponse<{
package_name: string package_name: string
package_usage_id: number package_usage_id: number
total_usage_mb: number total_usage_mb: number
@@ -429,6 +420,19 @@ export class CardService extends BaseService {
daily_usage_mb: number daily_usage_mb: number
cumulative_usage_mb: number cumulative_usage_mb: number
}> }>
}>>(`/api/admin/package-usage/${packageUsageId}/daily-records`) }>
> {
return this.get<
BaseResponse<{
package_name: string
package_usage_id: number
total_usage_mb: number
records: Array<{
date: string
daily_usage_mb: number
cumulative_usage_mb: number
}>
}>
>(`/api/admin/package-usage/${packageUsageId}/daily-records`)
} }
} }

View File

@@ -170,7 +170,7 @@ export class DeviceService extends BaseService {
*/ */
static rebootDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> { static rebootDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> {
return this.post<BaseResponse<DeviceOperationResponse>>( return this.post<BaseResponse<DeviceOperationResponse>>(
`/api/admin/devices/by-imei/${imei}/reboot`, `/api/admin/devices/by-identifier/${imei}/reboot`,
{} {}
) )
} }
@@ -181,7 +181,7 @@ export class DeviceService extends BaseService {
*/ */
static resetDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> { static resetDevice(imei: string): Promise<BaseResponse<DeviceOperationResponse>> {
return this.post<BaseResponse<DeviceOperationResponse>>( return this.post<BaseResponse<DeviceOperationResponse>>(
`/api/admin/devices/by-imei/${imei}/reset`, `/api/admin/devices/by-identifier/${imei}/reset`,
{} {}
) )
} }
@@ -196,7 +196,7 @@ export class DeviceService extends BaseService {
data: SetSpeedLimitRequest data: SetSpeedLimitRequest
): Promise<BaseResponse<DeviceOperationResponse>> { ): Promise<BaseResponse<DeviceOperationResponse>> {
return this.put<BaseResponse<DeviceOperationResponse>>( return this.put<BaseResponse<DeviceOperationResponse>>(
`/api/admin/devices/by-imei/${imei}/speed-limit`, `/api/admin/devices/by-identifier/${imei}/speed-limit`,
data data
) )
} }
@@ -211,7 +211,7 @@ export class DeviceService extends BaseService {
data: SwitchCardRequest data: SwitchCardRequest
): Promise<BaseResponse<DeviceOperationResponse>> { ): Promise<BaseResponse<DeviceOperationResponse>> {
return this.post<BaseResponse<DeviceOperationResponse>>( return this.post<BaseResponse<DeviceOperationResponse>>(
`/api/admin/devices/by-imei/${imei}/switch-card`, `/api/admin/devices/by-identifier/${imei}/switch-card`,
data data
) )
} }
@@ -226,7 +226,7 @@ export class DeviceService extends BaseService {
data: SetWiFiRequest data: SetWiFiRequest
): Promise<BaseResponse<DeviceOperationResponse>> { ): Promise<BaseResponse<DeviceOperationResponse>> {
return this.put<BaseResponse<DeviceOperationResponse>>( return this.put<BaseResponse<DeviceOperationResponse>>(
`/api/admin/devices/by-imei/${imei}/wifi`, `/api/admin/devices/by-identifier/${imei}/wifi`,
data data
) )
} }

View File

@@ -65,7 +65,9 @@ export class ExchangeService extends BaseService {
*/ */
static getExchanges( static getExchanges(
params?: ExchangeQueryParams params?: ExchangeQueryParams
): Promise<BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>> { ): Promise<
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
> {
return this.get< return this.get<
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }> BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
>('/api/admin/exchanges', params) >('/api/admin/exchanges', params)
@@ -123,7 +125,10 @@ export class ExchangeService extends BaseService {
* @param id 换货单ID * @param id 换货单ID
* @param data 发货参数 * @param data 发货参数
*/ */
static shipExchange(id: number, data: ShipExchangeRequest): Promise<BaseResponse<ExchangeResponse>> { static shipExchange(
id: number,
data: ShipExchangeRequest
): Promise<BaseResponse<ExchangeResponse>> {
return this.post<BaseResponse<ExchangeResponse>>(`/api/admin/exchanges/${id}/ship`, data) return this.post<BaseResponse<ExchangeResponse>>(`/api/admin/exchanges/${id}/ship`, data)
} }
} }

View File

@@ -20,9 +20,7 @@ export class ManualTriggerService extends BaseService {
* 批量手动触发 * 批量手动触发
* POST /api/admin/polling-manual-trigger/batch * POST /api/admin/polling-manual-trigger/batch
*/ */
static batchTrigger( static batchTrigger(data: BatchManualTriggerRequest): Promise<BaseResponse<ManualTriggerLog>> {
data: BatchManualTriggerRequest
): Promise<BaseResponse<ManualTriggerLog>> {
return this.post<BaseResponse<ManualTriggerLog>>( return this.post<BaseResponse<ManualTriggerLog>>(
'/api/admin/polling-manual-trigger/batch', '/api/admin/polling-manual-trigger/batch',
data data
@@ -46,9 +44,7 @@ export class ManualTriggerService extends BaseService {
* 单卡手动触发 * 单卡手动触发
* POST /api/admin/polling-manual-trigger/single * POST /api/admin/polling-manual-trigger/single
*/ */
static singleTrigger( static singleTrigger(data: SingleManualTriggerRequest): Promise<BaseResponse<ManualTriggerLog>> {
data: SingleManualTriggerRequest
): Promise<BaseResponse<ManualTriggerLog>> {
return this.post<BaseResponse<ManualTriggerLog>>( return this.post<BaseResponse<ManualTriggerLog>>(
'/api/admin/polling-manual-trigger/single', '/api/admin/polling-manual-trigger/single',
data data

View File

@@ -51,12 +51,7 @@ export class OrderService extends BaseService {
* 套餐购买预检 * 套餐购买预检
* @param data 预检请求参数 * @param data 预检请求参数
*/ */
static purchaseCheck( static purchaseCheck(data: PurchaseCheckRequest): Promise<BaseResponse<PurchaseCheckResponse>> {
data: PurchaseCheckRequest return this.post<BaseResponse<PurchaseCheckResponse>>('/api/admin/orders/purchase-check', data)
): Promise<BaseResponse<PurchaseCheckResponse>> {
return this.post<BaseResponse<PurchaseCheckResponse>>(
'/api/admin/orders/purchase-check',
data
)
} }
} }

View File

@@ -36,7 +36,10 @@ export class PollingConcurrencyService extends BaseService {
* @param taskType 任务类型 * @param taskType 任务类型
* @param params 更新参数 * @param params 更新参数
*/ */
static updateConcurrency(taskType: string, params: UpdateConcurrencyParams): Promise<BaseResponse<void>> { static updateConcurrency(
taskType: string,
params: UpdateConcurrencyParams
): Promise<BaseResponse<void>> {
return this.put<BaseResponse<void>>(`/api/admin/polling-concurrency/${taskType}`, params) return this.put<BaseResponse<void>>(`/api/admin/polling-concurrency/${taskType}`, params)
} }

View File

@@ -41,9 +41,7 @@ export class ShopSeriesGrantService extends BaseService {
* GET /api/admin/shop-series-grants/{id} * GET /api/admin/shop-series-grants/{id}
* @param id 授权ID * @param id 授权ID
*/ */
static getShopSeriesGrantDetail( static getShopSeriesGrantDetail(id: number): Promise<BaseResponse<ShopSeriesGrantResponse>> {
id: number
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`) return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`)
} }
@@ -57,10 +55,7 @@ export class ShopSeriesGrantService extends BaseService {
id: number, id: number,
data: UpdateShopSeriesGrantRequest data: UpdateShopSeriesGrantRequest
): Promise<BaseResponse<ShopSeriesGrantResponse>> { ): Promise<BaseResponse<ShopSeriesGrantResponse>> {
return this.update<ShopSeriesGrantResponse>( return this.update<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`, data)
`/api/admin/shop-series-grants/${id}`,
data
)
} }
/** /**

View File

@@ -32,9 +32,7 @@ export class WechatConfigService extends BaseService {
/** /**
* 创建支付配置 * 创建支付配置
*/ */
static createWechatConfig( static createWechatConfig(data: CreateWechatConfigRequest): Promise<BaseResponse<WechatConfig>> {
data: CreateWechatConfigRequest
): Promise<BaseResponse<WechatConfig>> {
return this.post<BaseResponse<WechatConfig>>('/api/admin/wechat-configs', data) return this.post<BaseResponse<WechatConfig>>('/api/admin/wechat-configs', data)
} }

View File

@@ -99,7 +99,7 @@
ElMessage.success('操作成功') ElMessage.success('操作成功')
} catch (error) { } catch (error) {
console.error('批量操作失败:', error) console.error('批量操作失败:', error)
ElMessage.error('操作失败') console.log('操作失败')
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -166,7 +166,9 @@
label: '用户类型', label: '用户类型',
width: 110, width: 110,
formatter: (row: PlatformAccount) => { formatter: (row: PlatformAccount) => {
return h(ElTag, { type: getUserTypeTag(row.user_type) }, () => getUserTypeName(row.user_type)) return h(ElTag, { type: getUserTypeTag(row.user_type) }, () =>
getUserTypeName(row.user_type)
)
} }
}, },
{ {

View File

@@ -30,130 +30,130 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ElCard } from 'element-plus' import { ElCard } from 'element-plus'
export interface DetailField { export interface DetailField {
label: string // 字段标签 label: string // 字段标签
prop?: string // 数据属性路径,支持点号分隔的嵌套路径,如 'one_time_commission_config.enable' prop?: string // 数据属性路径,支持点号分隔的嵌套路径,如 'one_time_commission_config.enable'
formatter?: (value: any, data: any) => string // 自定义格式化函数 formatter?: (value: any, data: any) => string // 自定义格式化函数
render?: (data: any) => any // 自定义渲染函数,返回 VNode render?: (data: any) => any // 自定义渲染函数,返回 VNode
fullWidth?: boolean // 是否占据整行 fullWidth?: boolean // 是否占据整行
}
export interface DetailSection {
title: string // 分组标题
fields: DetailField[] // 字段列表
columns?: 1 | 2 // 列数默认2列
}
interface Props {
sections: DetailSection[] // 详情页分组配置
data: any // 详情数据
}
const props = defineProps<Props>()
/**
* 根据点号分隔的路径获取嵌套对象的值
*/
const getNestedValue = (obj: any, path: string): any => {
if (!path) return obj
return path.split('.').reduce((acc, part) => acc?.[part], obj)
}
/**
* 格式化字段值
*/
const formatFieldValue = (field: DetailField, data: any): string => {
const value = field.prop ? getNestedValue(data, field.prop) : data
// 如果有自定义格式化函数
if (field.formatter) {
return field.formatter(value, data)
} }
// 默认处理 export interface DetailSection {
if (value === null || value === undefined || value === '') { title: string // 分组标题
return '-' fields: DetailField[] // 字段列表
columns?: 1 | 2 // 列数默认2列
} }
return String(value) interface Props {
} sections: DetailSection[] // 详情页分组配置
data: any // 详情数据
}
const props = defineProps<Props>()
/**
* 根据点号分隔的路径获取嵌套对象的值
*/
const getNestedValue = (obj: any, path: string): any => {
if (!path) return obj
return path.split('.').reduce((acc, part) => acc?.[part], obj)
}
/**
* 格式化字段值
*/
const formatFieldValue = (field: DetailField, data: any): string => {
const value = field.prop ? getNestedValue(data, field.prop) : data
// 如果有自定义格式化函数
if (field.formatter) {
return field.formatter(value, data)
}
// 默认处理
if (value === null || value === undefined || value === '') {
return '-'
}
return String(value)
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.detail-container { .detail-container {
max-height: 70vh; max-height: 70vh;
overflow-y: auto; padding: 4px;
padding: 4px; overflow-y: auto;
}
.detail-section {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
} }
.section-title { .detail-section {
font-size: 16px; margin-bottom: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.section-fields { &:last-child {
display: grid; margin-bottom: 0;
gap: 16px 24px; }
&.single-column { .section-title {
grid-template-columns: 1fr; font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
} }
&.double-column {
grid-template-columns: repeat(2, 1fr);
}
}
.field-item {
display: flex;
align-items: flex-start;
min-height: 32px;
&.full-width {
grid-column: 1 / -1;
}
.field-label {
flex-shrink: 0;
width: 140px;
font-weight: 500;
color: var(--el-text-color-regular);
line-height: 32px;
}
.field-value {
flex: 1;
color: var(--el-text-color-primary);
line-height: 32px;
word-break: break-word;
}
}
@media (max-width: 768px) {
.section-fields { .section-fields {
&.double-column { display: grid;
gap: 16px 24px;
&.single-column {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
&.double-column {
grid-template-columns: repeat(2, 1fr);
}
} }
.field-item { .field-item {
flex-direction: column; display: flex;
align-items: flex-start;
min-height: 32px;
&.full-width {
grid-column: 1 / -1;
}
.field-label { .field-label {
width: 100%; flex-shrink: 0;
margin-bottom: 4px; width: 140px;
font-weight: 500;
line-height: 32px;
color: var(--el-text-color-regular);
}
.field-value {
flex: 1;
line-height: 32px;
color: var(--el-text-color-primary);
word-break: break-word;
}
}
@media (width <= 768px) {
.section-fields {
&.double-column {
grid-template-columns: 1fr;
}
}
.field-item {
flex-direction: column;
.field-label {
width: 100%;
margin-bottom: 4px;
}
} }
} }
}
</style> </style>

View File

@@ -255,7 +255,7 @@
user-select: none; user-select: none;
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
&:hover:not(.is-disabled):not(.no-permission) { &:hover:not(.is-disabled, .no-permission) {
background-color: rgba(var(--art-gray-200-rgb), 0.7); background-color: rgba(var(--art-gray-200-rgb), 0.7);
} }
@@ -267,8 +267,8 @@
.menu-label { .menu-label {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
text-align: center; text-align: center;
white-space: normal;
word-break: break-all; word-break: break-all;
white-space: normal;
} }
} }

View File

@@ -24,15 +24,15 @@
<style scoped lang="scss"> <style scoped lang="scss">
.table-context-menu-hint { .table-context-menu-hint {
position: fixed; position: fixed;
padding: 4px 10px;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 12px;
border-radius: 4px;
pointer-events: none;
white-space: nowrap;
z-index: 9999; z-index: 9999;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); padding: 4px 10px;
font-size: 12px;
color: #fff;
white-space: nowrap;
pointer-events: none;
background-color: rgb(0 0 0 / 80%);
border-radius: 4px;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
</style> </style>

View File

@@ -1,11 +1,6 @@
<template> <template>
<ElDialog v-model="visible" title="设置WiFi" width="500px" @close="handleClose"> <ElDialog v-model="visible" title="设置WiFi" width="500px" @close="handleClose">
<ElForm <ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<ElFormItem label="设备信息"> <ElFormItem label="设备信息">
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span> <span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
</ElFormItem> </ElFormItem>
@@ -96,23 +91,29 @@
] ]
}) })
watch(() => props.modelValue, (val) => { watch(
visible.value = val () => props.modelValue,
if (val) { (val) => {
// 重置表单 visible.value = val
formData.enabled = 1 if (val) {
formData.ssid = '' // 重置表单
formData.password = '' formData.enabled = 1
formData.ssid = ''
formData.password = ''
}
} }
}) )
watch(visible, (val) => { watch(visible, (val) => {
emit('update:modelValue', val) emit('update:modelValue', val)
}) })
watch(() => props.loading, (val) => { watch(
confirmLoading.value = val () => props.loading,
}) (val) => {
confirmLoading.value = val
}
)
const handleClose = () => { const handleClose = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
@@ -122,7 +123,7 @@
if (!formRef.value) return if (!formRef.value) return
await formRef.value.validate((valid) => { await formRef.value.validate((valid) => {
if (valid) { if (valid) {
emit('confirm', { emit('confirm', {
enabled: formData.enabled, enabled: formData.enabled,
ssid: formData.ssid, ssid: formData.ssid,

View File

@@ -1,11 +1,6 @@
<template> <template>
<ElDialog v-model="visible" title="设置限速" width="500px" @close="handleClose"> <ElDialog v-model="visible" title="设置限速" width="500px" @close="handleClose">
<ElForm <ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<ElFormItem label="设备信息"> <ElFormItem label="设备信息">
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span> <span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
</ElFormItem> </ElFormItem>
@@ -17,7 +12,7 @@
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
/> />
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div> <div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
</ElFormItem> </ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed"> <ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber <ElInputNumber
@@ -27,7 +22,7 @@
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
/> />
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div> <div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
@@ -75,22 +70,28 @@
upload_speed: [{ required: true, message: '请输入上行速率', trigger: 'blur' }] upload_speed: [{ required: true, message: '请输入上行速率', trigger: 'blur' }]
}) })
watch(() => props.modelValue, (val) => { watch(
visible.value = val () => props.modelValue,
if (val) { (val) => {
// 重置表单 visible.value = val
formData.download_speed = 1024 if (val) {
formData.upload_speed = 512 // 重置表单
formData.download_speed = 1024
formData.upload_speed = 512
}
} }
}) )
watch(visible, (val) => { watch(visible, (val) => {
emit('update:modelValue', val) emit('update:modelValue', val)
}) })
watch(() => props.loading, (val) => { watch(
confirmLoading.value = val () => props.loading,
}) (val) => {
confirmLoading.value = val
}
)
const handleClose = () => { const handleClose = () => {
formRef.value?.resetFields() formRef.value?.resetFields()

View File

@@ -1,6 +1,6 @@
<template> <template>
<ElDialog v-model="visible" title="切换SIM卡" width="600px" @close="handleClose"> <ElDialog v-model="visible" title="切换SIM卡" width="600px" @close="handleClose">
<div v-if="loading" style="text-align: center; padding: 40px"> <div v-if="loading" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div> <div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
</div> </div>
@@ -42,7 +42,7 @@
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`" :label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
:value="card.iccid" :value="card.iccid"
> >
<div style="display: flex; justify-content: space-between; align-items: center"> <div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ card.iccid }}</span> <span>{{ card.iccid }}</span>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag> <ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
</div> </div>
@@ -123,13 +123,16 @@
target_iccid: [{ required: true, message: '请选择目标ICCID', trigger: 'change' }] target_iccid: [{ required: true, message: '请选择目标ICCID', trigger: 'change' }]
}) })
watch(() => props.modelValue, (val) => { watch(
visible.value = val () => props.modelValue,
if (val) { (val) => {
// 重置表单 visible.value = val
formData.target_iccid = '' if (val) {
// 重置表单
formData.target_iccid = ''
}
} }
}) )
watch(visible, (val) => { watch(visible, (val) => {
emit('update:modelValue', val) emit('update:modelValue', val)

View File

@@ -28,9 +28,7 @@ export const useAuth = () => {
if (Array.isArray(permission)) { if (Array.isArray(permission)) {
// 多个权限(满足任意一个即可) // 多个权限(满足任意一个即可)
return permission.some( return permission.some((perm) => userStore.hasPermission(perm) || userStore.hasButton(perm))
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
)
} }
return false return false
@@ -45,9 +43,7 @@ export const useAuth = () => {
if (!permissions || !Array.isArray(permissions)) return false if (!permissions || !Array.isArray(permissions)) return false
// 需要全部满足 // 需要全部满足
return permissions.every( return permissions.every((perm) => userStore.hasPermission(perm) || userStore.hasButton(perm))
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
)
} }
return { return {

View File

@@ -94,7 +94,10 @@ export function useCheckedColumns(columnsFactory: () => ColumnOption[]) {
cols.forEach((column) => { cols.forEach((column) => {
// 为普通列添加空值处理 // 为普通列添加空值处理
if (!column.type || (column.type !== 'selection' && column.type !== 'expand' && column.type !== 'index')) { if (
!column.type ||
(column.type !== 'selection' && column.type !== 'expand' && column.type !== 'index')
) {
const originalFormatter = column.formatter const originalFormatter = column.formatter
// 包装原有的 formatter,在其基础上添加空值处理 // 包装原有的 formatter,在其基础上添加空值处理

View File

@@ -21,12 +21,7 @@ export function useTableContextMenu() {
/** /**
* 单元格鼠标进入事件处理 * 单元格鼠标进入事件处理
*/ */
const handleCellMouseEnter = ( const handleCellMouseEnter = (row: any, column: any, cell: HTMLElement, event: MouseEvent) => {
row: any,
column: any,
cell: HTMLElement,
event: MouseEvent
) => {
// 清除之前的定时器 // 清除之前的定时器
if (hintTimer) { if (hintTimer) {
clearTimeout(hintTimer) clearTimeout(hintTimer)

View File

@@ -7,19 +7,19 @@
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 ElButton2: (typeof import('element-plus/es'))['ElButton2']
const ElDropdown: typeof import('element-plus/es')['ElDropdown'] const ElDropdown: (typeof import('element-plus/es'))['ElDropdown']
const ElDropdown2: typeof import('element-plus/es')['ElDropdown2'] const ElDropdown2: (typeof import('element-plus/es'))['ElDropdown2']
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] const ElDropdownItem: (typeof import('element-plus/es'))['ElDropdownItem']
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 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']
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm'] const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
const ElPopover: (typeof import('element-plus/es'))['ElPopover'] const ElPopover: (typeof import('element-plus/es'))['ElPopover']
const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn'] const ElTableColumn: (typeof import('element-plus/es'))['ElTableColumn']
const ElTag: typeof import('element-plus/es')['ElTag'] const ElTag: (typeof import('element-plus/es'))['ElTag']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef']

View File

@@ -888,8 +888,8 @@
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
.dialog-title { .dialog-title {
@@ -899,8 +899,8 @@
.account-info { .account-info {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
align-items: center;
.account-name { .account-name {
font-size: 14px; font-size: 14px;
@@ -911,25 +911,25 @@
.role-transfer-container { .role-transfer-container {
display: flex; display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px; gap: 20px;
padding: 20px 0; align-items: stretch;
justify-content: space-between;
min-height: 500px; min-height: 500px;
padding: 20px 0;
.transfer-panel { .transfer-panel {
flex: 1;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
max-width: 380px;
overflow: hidden;
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
border-radius: 4px; border-radius: 4px;
overflow: hidden;
max-width: 380px;
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
padding: 12px 16px; padding: 12px 16px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color); border-bottom: 1px solid var(--el-border-color);
@@ -943,8 +943,8 @@
.panel-body { .panel-body {
flex: 1; flex: 1;
overflow-y: auto;
padding: 12px; padding: 12px;
overflow-y: auto;
.role-list { .role-list {
display: flex; display: flex;
@@ -964,9 +964,9 @@
.role-info { .role-info {
display: flex; display: flex;
align-items: center;
gap: 8px;
flex: 1; flex: 1;
gap: 8px;
align-items: center;
} }
:deep(.el-checkbox) { :deep(.el-checkbox) {
@@ -980,9 +980,9 @@
.assigned-role-item { .assigned-role-item {
display: flex; display: flex;
justify-content: space-between;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: space-between;
.role-info { .role-info {
flex: 1; flex: 1;
@@ -999,9 +999,9 @@
.transfer-buttons { .transfer-buttons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: center;
padding: 0 10px; padding: 0 10px;
.el-button { .el-button {

View File

@@ -1065,8 +1065,8 @@
.card-selection-info { .card-selection-info {
margin-top: 10px; margin-top: 10px;
margin-bottom: 12px; margin-bottom: 12px;
color: var(--el-color-info);
font-size: 14px; font-size: 14px;
color: var(--el-color-info);
} }
.card-pagination { .card-pagination {

View File

@@ -18,7 +18,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton @click="showDialog('add')" v-permission="'platform_account:add'">新增平台账号</ElButton> <ElButton @click="showDialog('add')" v-permission="'platform_account:add'"
>新增平台账号</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -158,18 +160,11 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list"> <ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div <div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
v-for="role in filteredAvailableRoles"
:key="role.ID"
class="role-item"
>
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)"> <ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
:type="role.role_type === 1 ? 'primary' : 'success'"
size="small"
>
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span> </span>
@@ -212,19 +207,11 @@
> >
<span class="role-info"> <span class="role-info">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
:type="role.role_type === 1 ? 'primary' : 'success'"
size="small"
>
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span> </span>
<ElButton <ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
type="danger"
size="small"
link
@click="removeSingleRole(role.ID)"
>
移除 移除
</ElButton> </ElButton>
</div> </div>
@@ -956,8 +943,8 @@
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
.dialog-title { .dialog-title {
@@ -967,8 +954,8 @@
.account-info { .account-info {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
align-items: center;
.account-name { .account-name {
font-size: 14px; font-size: 14px;
@@ -979,25 +966,25 @@
.role-transfer-container { .role-transfer-container {
display: flex; display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px; gap: 20px;
padding: 20px 0; align-items: stretch;
justify-content: space-between;
min-height: 500px; min-height: 500px;
padding: 20px 0;
.transfer-panel { .transfer-panel {
flex: 1;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
max-width: 380px;
overflow: hidden;
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
border-radius: 4px; border-radius: 4px;
overflow: hidden;
max-width: 380px;
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
padding: 12px 16px; padding: 12px 16px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color); border-bottom: 1px solid var(--el-border-color);
@@ -1011,8 +998,8 @@
.panel-body { .panel-body {
flex: 1; flex: 1;
overflow-y: auto;
padding: 12px; padding: 12px;
overflow-y: auto;
.role-list { .role-list {
display: flex; display: flex;
@@ -1032,9 +1019,9 @@
.role-info { .role-info {
display: flex; display: flex;
align-items: center;
gap: 8px;
flex: 1; flex: 1;
gap: 8px;
align-items: center;
} }
:deep(.el-checkbox) { :deep(.el-checkbox) {
@@ -1048,9 +1035,9 @@
.assigned-role-item { .assigned-role-item {
display: flex; display: flex;
justify-content: space-between;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: space-between;
.role-info { .role-info {
flex: 1; flex: 1;
@@ -1067,9 +1054,9 @@
.transfer-buttons { .transfer-buttons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: center;
padding: 0 10px; padding: 0 10px;
.el-button { .el-button {

View File

@@ -17,7 +17,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton @click="showDialog('add')" v-permission="'shop_account:add'">新增代理账号</ElButton> <ElButton @click="showDialog('add')" v-permission="'shop_account:add'"
>新增代理账号</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>

View File

@@ -89,7 +89,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取资产分配详情失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -107,8 +106,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -122,10 +121,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -312,7 +312,7 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取分配记录列表失败') console.log('获取分配记录列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -382,8 +382,10 @@
const handleContextMenuSelect = (item: MenuItemType) => { const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return if (!currentRow.value) return
switch (item.key) { switch (
item.key
// No cases available // No cases available
) {
} }
} }
</script> </script>

View File

@@ -94,7 +94,7 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取授权记录详情失败') console.log('获取授权记录详情失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -112,8 +112,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -127,10 +127,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -343,7 +343,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取授权记录列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -417,7 +416,6 @@
getTableData() getTableData()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('备注修改失败')
} finally { } finally {
remarkLoading.value = false remarkLoading.value = false
} }

View File

@@ -284,7 +284,7 @@
<!-- 设备详情弹窗 --> <!-- 设备详情弹窗 -->
<ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="1000px"> <ElDialog v-model="deviceDetailDialogVisible" title="设备详情" width="1000px">
<div v-if="deviceDetailLoading" style="text-align: center; padding: 40px 0"> <div v-if="deviceDetailLoading" style=" padding: 40px 0;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
</div> </div>
<div v-else-if="currentDeviceDetail"> <div v-else-if="currentDeviceDetail">
@@ -398,14 +398,16 @@
</ElCard> </ElCard>
<!-- 已绑定卡片列表 --> <!-- 已绑定卡片列表 -->
<div v-if="deviceCardsLoading" style="text-align: center; padding: 40px 0"> <div v-if="deviceCardsLoading" style=" padding: 40px 0;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
</div> </div>
<div v-else> <div v-else>
<ElTable :data="deviceCards" border style="width: 100%"> <ElTable :data="deviceCards" border style="width: 100%">
<ElTableColumn prop="slot_position" label="插槽位置" width="120" align="center"> <ElTableColumn prop="slot_position" label="插槽位置" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div style="display: flex; align-items: center; justify-content: center; gap: 4px"> <div
style="display: flex; gap: 4px; align-items: center; justify-content: center"
>
<span>{{ row.slot_position }}</span> <span>{{ row.slot_position }}</span>
<ElTag v-if="row.is_current" type="success" size="small">当前</ElTag> <ElTag v-if="row.is_current" type="success" size="small">当前</ElTag>
</div> </div>
@@ -435,7 +437,7 @@
</ElTable> </ElTable>
<div <div
v-if="deviceCards.length === 0" v-if="deviceCards.length === 0"
style="text-align: center; padding: 20px; color: #909399" style=" padding: 20px; color: #909399;text-align: center"
> >
暂无设备绑定的卡 暂无设备绑定的卡
</div> </div>
@@ -451,7 +453,7 @@
/> />
<!-- 设置限速对话框 --> <!-- 设置限速对话框 -->
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="500px"> <ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="50%">
<ElForm <ElForm
ref="speedLimitFormRef" ref="speedLimitFormRef"
:model="speedLimitForm" :model="speedLimitForm"
@@ -469,7 +471,7 @@
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
/> />
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div> <div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
</ElFormItem> </ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed"> <ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber <ElInputNumber
@@ -479,7 +481,7 @@
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
/> />
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div> <div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
@@ -492,7 +494,7 @@
<!-- 切换SIM卡对话框 --> <!-- 切换SIM卡对话框 -->
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px"> <ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
<div v-if="loadingDeviceCards" style="text-align: center; padding: 40px"> <div v-if="loadingDeviceCards" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div> <div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
</div> </div>
@@ -534,9 +536,11 @@
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}${card.is_current ? ' (当前)' : ''}`" :label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}${card.is_current ? ' (当前)' : ''}`"
:value="card.iccid" :value="card.iccid"
> >
<div style="display: flex; justify-content: space-between; align-items: center"> <div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ card.iccid }}</span> <span>{{ card.iccid }}</span>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag> <ElTag size="small" style="margin-left: 10px"
>插槽{{ card.slot_position }}</ElTag
>
</div> </div>
</ElOption> </ElOption>
</ElSelect> </ElSelect>
@@ -1042,7 +1046,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('解绑卡失败:', error) console.error('解绑卡失败:', error)
ElMessage.error(error?.message || '解绑失败')
} }
}) })
.catch(() => { .catch(() => {
@@ -1214,13 +1217,15 @@
prop: 'last_online_time', prop: 'last_online_time',
label: '最后在线时间', label: '最后在线时间',
width: 180, width: 180,
formatter: (row: Device) => row.last_online_time ? formatDateTime(row.last_online_time) : '-' formatter: (row: Device) =>
row.last_online_time ? formatDateTime(row.last_online_time) : '-'
}, },
{ {
prop: 'last_gateway_sync_at', prop: 'last_gateway_sync_at',
label: '最后同步时间', label: '最后同步时间',
width: 180, width: 180,
formatter: (row: Device) => row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-' formatter: (row: Device) =>
row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-'
}, },
{ {
prop: 'batch_no', prop: 'batch_no',
@@ -1310,7 +1315,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取设备列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -1368,7 +1372,6 @@
await getTableData() await getTableData()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('删除失败')
} }
}) })
.catch(() => { .catch(() => {
@@ -1416,7 +1419,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('分配失败')
} finally { } finally {
allocateLoading.value = false allocateLoading.value = false
} }
@@ -1467,7 +1469,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('回收失败')
} finally { } finally {
recallLoading.value = false recallLoading.value = false
} }
@@ -1510,7 +1511,6 @@
} }
} catch (error) { } catch (error) {
console.error('获取套餐系列列表失败:', error) console.error('获取套餐系列列表失败:', error)
ElMessage.error('获取套餐系列列表失败')
} finally { } finally {
seriesLoading.value = false seriesLoading.value = false
} }
@@ -1711,7 +1711,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('重启设备失败:', error) console.error('重启设备失败:', error)
ElMessage.error(error?.message || '重启失败')
} }
}) })
.catch(() => { .catch(() => {
@@ -1740,7 +1739,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('恢复出厂设置失败:', error) console.error('恢复出厂设置失败:', error)
ElMessage.error(error?.message || '操作失败')
} }
}) })
.catch(() => { .catch(() => {
@@ -1776,7 +1774,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('设置限速失败:', error) console.error('设置限速失败:', error)
ElMessage.error(error?.message || '设置失败')
} finally { } finally {
speedLimitLoading.value = false speedLimitLoading.value = false
} }
@@ -1818,7 +1815,6 @@
} }
} catch (error) { } catch (error) {
console.error('加载设备绑定卡列表失败:', error) console.error('加载设备绑定卡列表失败:', error)
ElMessage.error('加载卡列表失败')
} finally { } finally {
loadingDeviceCards.value = false loadingDeviceCards.value = false
} }
@@ -1843,7 +1839,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('切换SIM卡失败:', error) console.error('切换SIM卡失败:', error)
ElMessage.error(error?.message || '切换失败')
} finally { } finally {
switchCardLoading.value = false switchCardLoading.value = false
} }
@@ -1881,7 +1876,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('设置WiFi失败:', error) console.error('设置WiFi失败:', error)
ElMessage.error(error?.message || '设置失败')
} finally { } finally {
setWiFiLoading.value = false setWiFiLoading.value = false
} }
@@ -1915,12 +1909,12 @@
}) })
} }
if (hasAuth('device:set_speed_limit')) { // if (hasAuth('device:set_speed_limit')) {
items.push({ // items.push({
key: 'speed-limit', // key: 'speed-limit',
label: '设置限速' // label: '设置限速'
}) // })
} // }
if (hasAuth('device:switch_sim')) { if (hasAuth('device:switch_sim')) {
items.push({ items.push({

View File

@@ -135,7 +135,7 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('查询设备详情失败:', error) console.error('查询设备详情失败:', error)
ElMessage.error(error?.message || '查询失败,请检查设备号是否正确') console.log(error?.message || '查询失败,请检查设备号是否正确')
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -506,7 +506,6 @@
ElMessage.success('设备导入模板下载成功') ElMessage.success('设备导入模板下载成功')
} catch (error) { } catch (error) {
console.error('下载模板失败:', error) console.error('下载模板失败:', error)
ElMessage.error('下载模板失败')
} }
} }
@@ -595,7 +594,6 @@
}) })
} catch (error: any) { } catch (error: any) {
console.error('设备导入失败:', error) console.error('设备导入失败:', error)
ElMessage.error(error.message || '设备导入失败')
} finally { } finally {
uploading.value = false uploading.value = false
} }
@@ -645,7 +643,6 @@
} }
} catch (error) { } catch (error) {
console.error('下载失败数据失败:', error) console.error('下载失败数据失败:', error)
ElMessage.error('下载失败数据失败')
} }
} }

View File

@@ -539,7 +539,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -607,7 +606,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取设备列表失败')
} finally { } finally {
availableDevicesLoading.value = false availableDevicesLoading.value = false
} }
@@ -699,7 +697,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
} finally { } finally {
allocateLoading.value = false allocateLoading.value = false
} }
@@ -799,8 +796,8 @@
.device-selection-info { .device-selection-info {
margin-top: 10px; margin-top: 10px;
margin-bottom: 12px; margin-bottom: 12px;
color: var(--el-color-info);
font-size: 14px; font-size: 14px;
color: var(--el-color-info);
} }
} }
</style> </style>

View File

@@ -21,7 +21,6 @@
<span>加载中...</span> <span>加载中...</span>
</div> </div>
</ElCard> </ElCard>
</div> </div>
</template> </template>
@@ -64,8 +63,7 @@
{ label: '换货单号', prop: 'exchange_no' }, { label: '换货单号', prop: 'exchange_no' },
{ {
label: '状态', label: '状态',
render: (data) => render: (data) => h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
}, },
{ {
label: '换货原因', label: '换货原因',
@@ -80,11 +78,7 @@
{ {
label: '新资产类型', label: '新资产类型',
formatter: (_, data) => formatter: (_, data) =>
data.new_asset_type data.new_asset_type ? (data.new_asset_type === 'iot_card' ? 'IoT卡' : '设备') : '--'
? data.new_asset_type === 'iot_card'
? 'IoT卡'
: '设备'
: '--'
}, },
{ {
label: '新资产标识符', label: '新资产标识符',

View File

@@ -13,16 +13,9 @@
<ElCard shadow="never" class="art-table-card"> <ElCard shadow="never" class="art-table-card">
<!-- 表格头部 --> <!-- 表格头部 -->
<ArtTableHeader <ArtTableHeader v-model:columns="columnChecks" @refresh="handleRefresh">
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left> <template #left>
<ElButton <ElButton type="primary" @click="showCreateDialog" v-permission="'exchange:create'">
type="primary"
@click="showCreateDialog"
v-permission="'exchange:create'"
>
创建换货单 创建换货单
</ElButton> </ElButton>
</template> </template>
@@ -68,12 +61,7 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleCloseCreateDialog" @closed="handleCloseCreateDialog"
> >
<ElForm <ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
ref="createFormRef"
:model="createForm"
:rules="createRules"
label-width="120px"
>
<ElFormItem label="换货原因" prop="exchange_reason"> <ElFormItem label="换货原因" prop="exchange_reason">
<ElInput <ElInput
v-model="createForm.exchange_reason" v-model="createForm.exchange_reason"
@@ -83,7 +71,11 @@
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="旧资产类型" prop="old_asset_type"> <ElFormItem label="旧资产类型" prop="old_asset_type">
<ElSelect v-model="createForm.old_asset_type" placeholder="请选择旧资产类型" style="width: 100%"> <ElSelect
v-model="createForm.old_asset_type"
placeholder="请选择旧资产类型"
style="width: 100%"
>
<ElOption label="IoT卡" value="iot_card" /> <ElOption label="IoT卡" value="iot_card" />
<ElOption label="设备" value="device" /> <ElOption label="设备" value="device" />
</ElSelect> </ElSelect>
@@ -122,12 +114,7 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleCloseShipDialog" @closed="handleCloseShipDialog"
> >
<ElForm <ElForm ref="shipFormRef" :model="shipForm" :rules="shipRules" label-width="120px">
ref="shipFormRef"
:model="shipForm"
:rules="shipRules"
label-width="120px"
>
<ElFormItem label="新资产标识符" prop="new_identifier"> <ElFormItem label="新资产标识符" prop="new_identifier">
<ElInput <ElInput
v-model="shipForm.new_identifier" v-model="shipForm.new_identifier"
@@ -142,7 +129,7 @@
</ElFormItem> </ElFormItem>
<ElFormItem label="是否迁移数据"> <ElFormItem label="是否迁移数据">
<ElSwitch v-model="shipForm.migrate_data" /> <ElSwitch v-model="shipForm.migrate_data" />
<div style="font-size: 12px; color: #909399; margin-top: 4px"> <div style=" margin-top: 4px;font-size: 12px; color: #909399">
开启后将迁移钱包余额套餐记录到新资产 开启后将迁移钱包余额套餐记录到新资产
</div> </div>
</ElFormItem> </ElFormItem>
@@ -185,12 +172,8 @@
const router = useRouter() const router = useRouter()
// 右键菜单相关 // 右键菜单相关
const { const { showContextMenuHint, hintPosition, handleCellMouseEnter, handleCellMouseLeave } =
showContextMenuHint, useTableContextMenu()
hintPosition,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>() const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentExchangeRow = ref<ExchangeResponse | null>(null) const currentExchangeRow = ref<ExchangeResponse | null>(null)
@@ -334,15 +317,14 @@
prop: 'status', prop: 'status',
label: '状态', label: '状态',
width: 130, width: 130,
formatter: (row: any) => formatter: (row: any) => h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
}, },
{ {
prop: 'created_at', prop: 'created_at',
label: '创建时间', label: '创建时间',
width: 180, width: 180,
formatter: (row: any) => formatDateTime(row.created_at) formatter: (row: any) => formatDateTime(row.created_at)
}, }
]) ])
const getStatusType = (status: number) => { const getStatusType = (status: number) => {

View File

@@ -385,7 +385,7 @@
<!-- 卡详情对话框 --> <!-- 卡详情对话框 -->
<ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px"> <ElDialog v-model="cardDetailDialogVisible" title="卡片详情" width="900px">
<div v-if="cardDetailLoading" style="text-align: center; padding: 40px"> <div v-if="cardDetailLoading" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载中...</div> <div style="margin-top: 16px">加载中...</div>
</div> </div>
@@ -465,7 +465,7 @@
<!-- 流量使用查询对话框 --> <!-- 流量使用查询对话框 -->
<ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px"> <ElDialog v-model="flowUsageDialogVisible" title="流量使用查询" width="500px">
<div v-if="flowUsageLoading" style="text-align: center; padding: 40px"> <div v-if="flowUsageLoading" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div> <div style="margin-top: 16px">查询中...</div>
</div> </div>
@@ -494,7 +494,7 @@
<!-- 实名状态查询对话框 --> <!-- 实名状态查询对话框 -->
<ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px"> <ElDialog v-model="realnameStatusDialogVisible" title="实名认证状态" width="500px">
<div v-if="realnameStatusLoading" style="text-align: center; padding: 40px"> <div v-if="realnameStatusLoading" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div> <div style="margin-top: 16px">查询中...</div>
</div> </div>
@@ -517,7 +517,7 @@
<!-- 卡实时状态查询对话框 --> <!-- 卡实时状态查询对话框 -->
<ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px"> <ElDialog v-model="cardStatusDialogVisible" title="卡实时状态" width="500px">
<div v-if="cardStatusLoading" style="text-align: center; padding: 40px"> <div v-if="cardStatusLoading" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">查询中...</div> <div style="margin-top: 16px">查询中...</div>
</div> </div>
@@ -1108,7 +1108,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取单卡列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -1364,7 +1363,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('批量回收失败,请重试')
} finally { } finally {
recallLoading.value = false recallLoading.value = false
} }
@@ -1407,7 +1405,6 @@
} }
} catch (error) { } catch (error) {
console.error('获取套餐系列列表失败:', error) console.error('获取套餐系列列表失败:', error)
ElMessage.error('获取套餐系列列表失败')
} finally { } finally {
seriesLoading.value = false seriesLoading.value = false
} }
@@ -1475,7 +1472,6 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('批量设置套餐系列失败,请重试')
} finally { } finally {
seriesBindingLoading.value = false seriesBindingLoading.value = false
} }
@@ -1487,27 +1483,6 @@
const moreMenuItems = computed((): MenuItemType[] => { const moreMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = [] const items: MenuItemType[] = []
if (hasAuth('iot_card:network_distribution')) {
items.push({
key: 'distribution',
label: '网卡分销'
})
}
if (hasAuth('iot_card:batch_recharge')) {
items.push({
key: 'recharge',
label: '批量充值'
})
}
if (hasAuth('iot_card:network_recycle')) {
items.push({
key: 'recycle',
label: '网卡回收'
})
}
if (hasAuth('iot_card:batch_download')) { if (hasAuth('iot_card:batch_download')) {
items.push({ items.push({
key: 'download', key: 'download',
@@ -1518,7 +1493,7 @@
if (hasAuth('iot_card:change_package')) { if (hasAuth('iot_card:change_package')) {
items.push({ items.push({
key: 'changePackage', key: 'changePackage',
label: '变更套餐' label: '其他功能暂定'
}) })
} }
@@ -1698,7 +1673,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('查询流量使用失败:', error) console.error('查询流量使用失败:', error)
ElMessage.error(error?.message || '查询失败')
flowUsageDialogVisible.value = false flowUsageDialogVisible.value = false
} finally { } finally {
flowUsageLoading.value = false flowUsageLoading.value = false
@@ -1735,7 +1709,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('查询实名状态失败:', error) console.error('查询实名状态失败:', error)
ElMessage.error(error?.message || '查询失败')
realnameStatusDialogVisible.value = false realnameStatusDialogVisible.value = false
} finally { } finally {
realnameStatusLoading.value = false realnameStatusLoading.value = false
@@ -1755,7 +1728,7 @@
// 直接使用 resolveAsset 返回的网络状态 // 直接使用 resolveAsset 返回的网络状态
cardStatusData.value = { cardStatusData.value = {
iccid: iccid, iccid: iccid,
cardStatus: res.data. === 1 ? '正常' : '停机', cardStatus: res.data.network_status === 1 ? '正常' : '停机',
extend: res.data.extend extend: res.data.extend
} }
} else { } else {
@@ -1764,7 +1737,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('查询卡状态失败:', error) console.error('查询卡状态失败:', error)
ElMessage.error(error?.message || '查询失败')
cardStatusDialogVisible.value = false cardStatusDialogVisible.value = false
} finally { } finally {
cardStatusLoading.value = false cardStatusLoading.value = false
@@ -1841,7 +1813,7 @@
const res = await CardService.deactivateIotCard(card.id) const res = await CardService.deactivateIotCard(card.id)
if (res.code === 0) { if (res.code === 0) {
ElMessage.success('手动停用成功') ElMessage.success('手动停用成功')
getTableData() await getTableData()
} }
} catch (error: any) { } catch (error: any) {
console.error('手动停用失败:', error) console.error('手动停用失败:', error)

View File

@@ -131,7 +131,6 @@
} }
} catch (error: any) { } catch (error: any) {
console.error('查询卡片详情失败:', error) console.error('查询卡片详情失败:', error)
ElMessage.error(error?.message || '查询失败请检查ICCID是否正确')
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -72,7 +72,7 @@
align-center align-center
destroy-on-close destroy-on-close
> >
<div v-if="failDataLoading" style="text-align: center; padding: 40px"> <div v-if="failDataLoading" style=" padding: 40px;text-align: center">
<ElSkeleton :rows="5" animated /> <ElSkeleton :rows="5" animated />
</div> </div>
<div v-else-if="failedItems.length > 0"> <div v-else-if="failedItems.length > 0">
@@ -92,7 +92,7 @@
<ElTableColumn prop="message" label="失败原因" min-width="200" show-overflow-tooltip /> <ElTableColumn prop="message" label="失败原因" min-width="200" show-overflow-tooltip />
</ElTable> </ElTable>
</div> </div>
<div v-else style="text-align: center; padding: 40px"> <div v-else style=" padding: 40px;text-align: center">
<ElEmpty description="暂无失败数据" /> <ElEmpty description="暂无失败数据" />
</div> </div>
@@ -555,7 +555,6 @@
} }
} catch (error) { } catch (error) {
console.error('获取失败数据失败:', error) console.error('获取失败数据失败:', error)
ElMessage.error('获取失败数据失败')
} finally { } finally {
failDataLoading.value = false failDataLoading.value = false
} }
@@ -594,7 +593,6 @@
ElMessage.success('失败数据下载成功') ElMessage.success('失败数据下载成功')
} catch (error) { } catch (error) {
console.error('下载失败数据失败:', error) console.error('下载失败数据失败:', error)
ElMessage.error('下载失败数据失败')
} }
} }
@@ -653,7 +651,6 @@
ElMessage.success('IoT卡导入模板下载成功') ElMessage.success('IoT卡导入模板下载成功')
} catch (error) { } catch (error) {
console.error('下载模板失败:', error) console.error('下载模板失败:', error)
ElMessage.error('下载模板失败')
} }
} }
@@ -774,7 +771,6 @@
}) })
} catch (error: any) { } catch (error: any) {
console.error('IoT卡导入失败:', error) console.error('IoT卡导入失败:', error)
ElMessage.error(error.message || 'IoT卡导入失败')
} finally { } finally {
uploading.value = false uploading.value = false
} }

View File

@@ -237,7 +237,7 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('获取任务详情失败') console.log('获取任务详情失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -252,8 +252,8 @@
.task-detail-page { .task-detail-page {
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {

View File

@@ -115,15 +115,7 @@
const { isDark, systemThemeType } = storeToRefs(settingStore) const { isDark, systemThemeType } = storeToRefs(settingStore)
// 使用登录 Composable // 使用登录 Composable
const { const { formRef, formData, rules, loading, mockAccounts, setupAccount, handleLogin } = useLogin()
formRef,
formData,
rules,
loading,
mockAccounts,
setupAccount,
handleLogin
} = useLogin()
const systemName = AppConfig.systemInfo.name const systemName = AppConfig.systemInfo.name

View File

@@ -644,7 +644,7 @@
// 刷新列表 // 刷新列表
getMyCardsList() getMyCardsList()
} catch (error) { } catch (error) {
ElMessage.error('分销操作失败,请重试') console.log('分销操作失败,请重试')
} }
} }
@@ -660,7 +660,7 @@
// 刷新列表 // 刷新列表
getMyCardsList() getMyCardsList()
} catch (error) { } catch (error) {
ElMessage.error('充值操作失败,请重试') console.log('充值操作失败,请重试')
} }
} }
@@ -677,7 +677,7 @@
tableData.value = tableData.value.filter((item) => !recycledIds.includes(item.id)) tableData.value = tableData.value.filter((item) => !recycledIds.includes(item.id))
selectedRows.value = [] selectedRows.value = []
} catch (error) { } catch (error) {
ElMessage.error('回收操作失败,请重试') console.log('回收操作失败,请重试')
} }
} }

View File

@@ -679,8 +679,8 @@
.account-list-page { .account-list-page {
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -694,8 +694,8 @@
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
.dialog-title { .dialog-title {
@@ -705,8 +705,8 @@
.account-info { .account-info {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
align-items: center;
.account-name { .account-name {
font-size: 14px; font-size: 14px;
@@ -717,25 +717,25 @@
.role-transfer-container { .role-transfer-container {
display: flex; display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px; gap: 20px;
padding: 20px 0; align-items: stretch;
justify-content: space-between;
min-height: 500px; min-height: 500px;
padding: 20px 0;
.transfer-panel { .transfer-panel {
flex: 1;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
max-width: 380px;
overflow: hidden;
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
border-radius: 4px; border-radius: 4px;
overflow: hidden;
max-width: 380px;
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
padding: 12px 16px; padding: 12px 16px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color); border-bottom: 1px solid var(--el-border-color);
@@ -749,8 +749,8 @@
.panel-body { .panel-body {
flex: 1; flex: 1;
overflow-y: auto;
padding: 12px; padding: 12px;
overflow-y: auto;
.role-list { .role-list {
display: flex; display: flex;
@@ -772,15 +772,15 @@
width: 100%; width: 100%;
.el-checkbox__label { .el-checkbox__label {
width: 100%;
display: flex; display: flex;
width: 100%;
.role-info { .role-info {
display: flex; display: flex;
flex: 1;
gap: 8px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px;
flex: 1;
width: 100%; width: 100%;
> span:first-child { > span:first-child {
@@ -794,16 +794,16 @@
.assigned-role-item { .assigned-role-item {
display: flex; display: flex;
justify-content: space-between;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: space-between;
.role-info { .role-info {
flex: 1;
display: flex; display: flex;
flex: 1;
gap: 8px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px;
> span:first-child { > span:first-child {
flex: 1; flex: 1;
@@ -825,9 +825,9 @@
.transfer-buttons { .transfer-buttons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: center;
padding: 0 10px; padding: 0 10px;
.el-button { .el-button {

View File

@@ -132,10 +132,10 @@
.card-header { .card-header {
display: flex; display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
align-items: center;
justify-content: space-between;
.header-left { .header-left {
display: flex; display: flex;
@@ -220,7 +220,7 @@
} }
} }
@media (max-width: 768px) { @media (width <= 768px) {
.active-wechat-config-widget { .active-wechat-config-widget {
.card-header { .card-header {
.header-left, .header-left,

View File

@@ -151,8 +151,8 @@
.stats-container { .stats-container {
display: flex; display: flex;
gap: 16px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px;
} }
.stat-card-wrapper { .stat-card-wrapper {
@@ -171,6 +171,7 @@
.stat-icon { .stat-icon {
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 40px; width: 40px;
@@ -178,7 +179,6 @@
font-size: 20px; font-size: 20px;
color: white; color: white;
border-radius: 8px; border-radius: 8px;
flex-shrink: 0;
} }
.stat-content { .stat-content {
@@ -193,10 +193,10 @@
} }
.stat-value { .stat-value {
overflow: hidden;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -204,7 +204,7 @@
} }
} }
@media (max-width: 1400px) { @media (width <= 1400px) {
.commission-summary-widget { .commission-summary-widget {
.stats-container { .stats-container {
gap: 12px; gap: 12px;
@@ -224,7 +224,7 @@
} }
} }
@media (max-width: 1200px) { @media (width <= 1200px) {
.commission-summary-widget { .commission-summary-widget {
.stat-card-wrapper { .stat-card-wrapper {
flex: 1 1 calc(33.333% - 12px); flex: 1 1 calc(33.333% - 12px);
@@ -233,7 +233,7 @@
} }
} }
@media (max-width: 768px) { @media (width <= 768px) {
.commission-summary-widget { .commission-summary-widget {
.stats-container { .stats-container {
gap: 12px; gap: 12px;
@@ -246,7 +246,7 @@
} }
} }
@media (max-width: 480px) { @media (width <= 480px) {
.commission-summary-widget { .commission-summary-widget {
.stat-card-wrapper { .stat-card-wrapper {
flex: 1 1 100%; flex: 1 1 100%;

View File

@@ -117,10 +117,10 @@
.card-header { .card-header {
display: flex; display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
align-items: center;
justify-content: space-between;
.header-left { .header-left {
display: flex; display: flex;
@@ -205,7 +205,7 @@
} }
} }
@media (max-width: 768px) { @media (width <= 768px) {
.withdrawal-settings-widget { .withdrawal-settings-widget {
.card-header { .card-header {
.header-left, .header-left,

View File

@@ -32,11 +32,7 @@
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
import { AgentRechargeService } from '@/api/modules' import { AgentRechargeService } from '@/api/modules'
import type { import type { AgentRecharge, AgentRechargeStatus, AgentRechargePaymentMethod } from '@/types/api'
AgentRecharge,
AgentRechargeStatus,
AgentRechargePaymentMethod
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias' import { RoutesAlias } from '@/router/routesAlias'
@@ -100,7 +96,8 @@
}, },
{ {
label: '状态', label: '状态',
render: (data) => h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status)) render: (data) =>
h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status))
} }
] ]
}, },
@@ -194,8 +191,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
margin-bottom: 24px; margin-bottom: 24px;
.detail-title { .detail-title {
@@ -209,10 +206,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 0; padding: 60px 0;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -259,21 +259,14 @@
> >
<ElAlert <ElAlert
:title=" :title="
resolveAction === 'release' resolveAction === 'release' ? '确认将该笔待审佣金记录入账' : '确认将该笔待审佣金记录作废'
? '确认将该笔待审佣金记录入账'
: '确认将该笔待审佣金记录作废'
" "
:type="resolveAction === 'release' ? 'success' : 'warning'" :type="resolveAction === 'release' ? 'success' : 'warning'"
style="margin-bottom: 16px" style="margin-bottom: 16px"
:closable="false" :closable="false"
/> />
<ElForm <ElForm ref="resolveFormRef" :model="resolveForm" :rules="resolveRules" label-width="100px">
ref="resolveFormRef"
:model="resolveForm"
:rules="resolveRules"
label-width="100px"
>
<ElFormItem v-if="resolveAction === 'release'" label="入账金额" prop="amount"> <ElFormItem v-if="resolveAction === 'release'" label="入账金额" prop="amount">
<ElInputNumber <ElInputNumber
v-model="resolveForm.amount" v-model="resolveForm.amount"
@@ -284,7 +277,7 @@
style="width: 100%" style="width: 100%"
placeholder="请输入入账金额(分)" placeholder="请输入入账金额(分)"
/> />
<div style="color: var(--el-text-color-secondary); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-secondary)">
金额单位为分例如100 = 10000 金额单位为分例如100 = 10000
</div> </div>
</ElFormItem> </ElFormItem>
@@ -736,7 +729,10 @@
} }
// 处理佣金修正 // 处理佣金修正
const handleResolveCommission = (row: ShopCommissionRecordItem, action: CommissionResolveAction) => { const handleResolveCommission = (
row: ShopCommissionRecordItem,
action: CommissionResolveAction
) => {
currentCommissionId.value = row.id currentCommissionId.value = row.id
resolveAction.value = action resolveAction.value = action
resolveForm.amount = undefined resolveForm.amount = undefined

View File

@@ -22,7 +22,12 @@
style="margin-top: 20px" style="margin-top: 20px"
> >
<template #left> <template #left>
<ElButton type="primary" @click="showWithdrawalDialog" v-permission="'my_commission:add'">发起提现</ElButton> <ElButton
type="primary"
@click="showWithdrawalDialog"
v-permission="'my_commission:add'"
>发起提现</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>

View File

@@ -10,7 +10,9 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton type="primary" @click="showDialog" v-permission="'withdrawal_settings:add'">新增配置</ElButton> <ElButton type="primary" @click="showDialog" v-permission="'withdrawal_settings:add'"
>新增配置</ElButton
>
</template> </template>
</ArtTableHeader> </ArtTableHeader>

View File

@@ -25,9 +25,7 @@
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="实际退款金额"> <ElDescriptionsItem label="实际退款金额">
{{ {{ refund.approved_refund_amount ? formatCurrency(refund.approved_refund_amount) : '-' }}
refund.approved_refund_amount ? formatCurrency(refund.approved_refund_amount) : '-'
}}
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="实收金额"> <ElDescriptionsItem label="实收金额">
{{ formatCurrency(refund.actual_received_amount) }} {{ formatCurrency(refund.actual_received_amount) }}
@@ -95,12 +93,7 @@
width="500px" width="500px"
@closed="handleApproveDialogClosed" @closed="handleApproveDialogClosed"
> >
<ElForm <ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
ref="approveFormRef"
:model="approveForm"
:rules="approveRules"
label-width="120px"
>
<ElFormItem label="退款单号"> <ElFormItem label="退款单号">
<span>{{ refund?.refund_no }}</span> <span>{{ refund?.refund_no }}</span>
</ElFormItem> </ElFormItem>
@@ -341,9 +334,7 @@
} }
// 获取状态标签类型 // 获取状态标签类型
const getStatusType = ( const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
status: RefundStatus
): 'warning' | 'success' | 'danger' | 'info' => {
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = { const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
1: 'warning', // 待审批 1: 'warning', // 待审批
2: 'success', // 已通过 2: 'success', // 已通过
@@ -551,15 +542,15 @@
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
} }
.action-buttons { .action-buttons {
margin-top: 20px;
display: flex; display: flex;
gap: 10px; gap: 10px;
justify-content: flex-end; justify-content: flex-end;
margin-top: 20px;
} }
} }
</style> </style>

View File

@@ -183,12 +183,7 @@
width="500px" width="500px"
@closed="handleRejectDialogClosed" @closed="handleRejectDialogClosed"
> >
<ElForm <ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
ref="rejectFormRef"
:model="rejectForm"
:rules="rejectRules"
label-width="120px"
>
<ElFormItem label="退款单号"> <ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span> <span>{{ currentRefund?.refund_no }}</span>
</ElFormItem> </ElFormItem>
@@ -220,12 +215,7 @@
width="500px" width="500px"
@closed="handleReturnDialogClosed" @closed="handleReturnDialogClosed"
> >
<ElForm <ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
ref="returnFormRef"
:model="returnForm"
:rules="returnRules"
label-width="120px"
>
<ElFormItem label="退款单号"> <ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span> <span>{{ currentRefund?.refund_no }}</span>
</ElFormItem> </ElFormItem>
@@ -460,9 +450,7 @@
const createRules = reactive<FormRules>({ const createRules = reactive<FormRules>({
order_id: [{ required: true, message: '请输入订单ID', trigger: 'blur' }], order_id: [{ required: true, message: '请输入订单ID', trigger: 'blur' }],
requested_refund_amount: [ requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }],
{ required: true, message: '请输入申请退款金额', trigger: 'blur' }
],
actual_received_amount: [{ required: true, message: '请输入实收金额', trigger: 'blur' }] actual_received_amount: [{ required: true, message: '请输入实收金额', trigger: 'blur' }]
}) })
@@ -519,9 +507,7 @@
} }
// 获取状态标签类型 // 获取状态标签类型
const getStatusType = ( const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
status: RefundStatus
): 'warning' | 'success' | 'danger' | 'info' => {
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = { const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
1: 'warning', // 待审批 1: 'warning', // 待审批
2: 'success', // 已通过 2: 'success', // 已通过
@@ -998,10 +984,7 @@
} }
// 已拒绝或已退回状态可以重新提交 // 已拒绝或已退回状态可以重新提交
if ( if (currentOperatingRefund.value.status === 3 || currentOperatingRefund.value.status === 4) {
currentOperatingRefund.value.status === 3 ||
currentOperatingRefund.value.status === 4
) {
items.push({ items.push({
key: 'resubmit', key: 'resubmit',
label: '重新提交' label: '重新提交'

View File

@@ -32,13 +32,19 @@
type="primary" type="primary"
@click="handleSearchCard" @click="handleSearchCard"
:loading="loading" :loading="loading"
:icon="Search"
style="margin-left: 16px" style="margin-left: 16px"
>查询</ElButton >查询</ElButton
> >
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
<div v-if="cardInfo" class="operation-button-group"> <div v-if="cardInfo" class="operation-button-group">
<ElButton @click="handleRefreshAsset" :loading="refreshLoading" type="primary"> <ElButton
@click="handleRefreshAsset"
:loading="refreshLoading"
type="primary"
:icon="Refresh"
>
刷新 刷新
</ElButton> </ElButton>
</div> </div>
@@ -60,7 +66,7 @@
</ElTag> </ElTag>
</div> </div>
</template> </template>
<ElDescriptions :column="3" border> <ElDescriptions :column="4" border>
<!--<ElDescriptionsItem label="资产状态">--> <!--<ElDescriptionsItem label="资产状态">-->
<!-- <ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">--> <!-- <ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">-->
<!-- {{ getAssetStatusName(cardInfo?.status) }}--> <!-- {{ getAssetStatusName(cardInfo?.status) }}-->
@@ -70,10 +76,9 @@
<!-- 卡专属字段 --> <!-- 卡专属字段 -->
<template v-if="cardInfo?.asset_type === 'card'"> <template v-if="cardInfo?.asset_type === 'card'">
<ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '--' }}</ElDescriptionsItem> <ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商账户">暂未接入</ElDescriptionsItem> <ElDescriptionsItem label="运营商账户">{{
<!--<ElDescriptionsItem label="运营商">{{--> cardInfo?.carrier_name || '--'
<!-- cardInfo?.carrier_name || '&#45;&#45;'--> }}</ElDescriptionsItem>
<!-- }}</ElDescriptionsItem>-->
<ElDescriptionsItem label="实名状态"> <ElDescriptionsItem label="实名状态">
<ElTag :type="getRealNameStatusType(cardInfo?.real_name_status)" size="small"> <ElTag :type="getRealNameStatusType(cardInfo?.real_name_status)" size="small">
{{ getRealNameStatusName(cardInfo?.real_name_status) }} {{ getRealNameStatusName(cardInfo?.real_name_status) }}
@@ -83,6 +88,9 @@
<ElDescriptionsItem label="店铺">{{ <ElDescriptionsItem label="店铺">{{
cardInfo?.shop_name || '--' cardInfo?.shop_name || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="套餐系列">{{
cardInfo?.series_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="虚拟号">{{ <ElDescriptionsItem label="虚拟号">{{
cardInfo?.virtual_no || '--' cardInfo?.virtual_no || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
@@ -155,7 +163,11 @@
cardInfo?.activated_at || '--' cardInfo?.activated_at || '--'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<ElDescriptionsItem label="实名时间">暂未接入</ElDescriptionsItem> <ElDescriptionsItem label="实名时间">暂未接入</ElDescriptionsItem>
<ElDescriptionsItem label="信号强度">暂未接入</ElDescriptionsItem> <ElDescriptionsItem label="信号强度">
<ElTag :type="getSignalStrengthType()" size="small">
{{ calculateSignalStrength() }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="在线状态"> <ElDescriptionsItem label="在线状态">
<ElTag :type="getOnlineStatusType(cardInfo?.online_status)" size="small"> <ElTag :type="getOnlineStatusType(cardInfo?.online_status)" size="small">
@@ -205,21 +217,22 @@
<ElTable <ElTable
v-if="cardInfo.cards && cardInfo.cards.length > 0" v-if="cardInfo.cards && cardInfo.cards.length > 0"
:data="cardInfo.cards" :data="cardInfo.cards"
border
style="margin-top: 16px" style="margin-top: 16px"
> >
<ElTableColumn prop="iccid" label="ICCID" min-width="180" /> <ElTableColumn prop="iccid" label="ICCID" />
<ElTableColumn prop="msisdn" label="MSISDN" min-width="120" /> <ElTableColumn prop="msisdn" label="MSISDN" />
<ElTableColumn prop="slot_position" label="卡槽位置" width="120" align="center"> <ElTableColumn prop="slot_position" label="卡槽位置" align="center">
<template #default="scope"> <template #default="scope">
<div <div
style="display: flex; align-items: center; justify-content: center; gap: 4px" style="display: flex; gap: 4px; align-items: center; justify-content: center"
> >
<span>{{ scope.row.slot_position }}</span> <span>{{ scope.row.slot_position }}</span>
<ElTag v-if="scope.row.is_current" type="success" size="small">当前</ElTag> <ElTag v-if="scope.row.is_current" type="success" size="small">当前</ElTag>
</div> </div>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="网络状态" width="100" align="center"> <ElTableColumn label="网络状态" align="center">
<template #default="scope"> <template #default="scope">
<ElTag <ElTag
:type="scope.row.network_status === 1 ? 'success' : 'danger'" :type="scope.row.network_status === 1 ? 'success' : 'danger'"
@@ -229,7 +242,7 @@
</ElTag> </ElTag>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="实名状态" width="100" align="center"> <ElTableColumn label="实名状态" align="center">
<template #default="scope"> <template #default="scope">
<ElTag :type="getRealNameStatusType(scope.row.real_name_status)" size="small"> <ElTag :type="getRealNameStatusType(scope.row.real_name_status)" size="small">
{{ getRealNameStatusName(scope.row.real_name_status) }} {{ getRealNameStatusName(scope.row.real_name_status) }}
@@ -242,7 +255,7 @@
<!-- 设备实时信息 --> <!-- 设备实时信息 -->
<ElDivider content-position="left">设备实时信息</ElDivider> <ElDivider content-position="left">设备实时信息</ElDivider>
<div v-if="deviceRealtime" style="margin-top: 16px"> <div v-if="deviceRealtime" style="margin-top: 16px">
<ElDescriptions :column="3" border size="small"> <ElDescriptions :column="4" border>
<!-- 基本状态 --> <!-- 基本状态 -->
<ElDescriptionsItem label="在线状态"> <ElDescriptionsItem label="在线状态">
<ElTag <ElTag
@@ -299,8 +312,8 @@
v-if="deviceRealtime.current_iccid" v-if="deviceRealtime.current_iccid"
style=" style="
color: var(--el-color-primary); color: var(--el-color-primary);
cursor: pointer;
text-decoration: underline; text-decoration: underline;
cursor: pointer;
" "
@click="handleViewCardDetail(deviceRealtime.current_iccid)" @click="handleViewCardDetail(deviceRealtime.current_iccid)"
> >
@@ -426,13 +439,13 @@
<ElButton type="warning" @click="handleResetDevice" :loading="resetDeviceLoading"> <ElButton type="warning" @click="handleResetDevice" :loading="resetDeviceLoading">
恢复出厂 恢复出厂
</ElButton> </ElButton>
<ElButton <!--<ElButton-->
type="info" <!-- type="info"-->
@click="handleShowSpeedLimitDialog" <!-- @click="handleShowSpeedLimitDialog"-->
:loading="speedLimitLoading" <!-- :loading="speedLimitLoading"-->
> <!--&gt;-->
设置限速 <!-- 设置限速-->
</ElButton> <!--</ElButton>-->
<ElButton <ElButton
type="success" type="success"
@click="handleShowSwitchCardDialog" @click="handleShowSwitchCardDialog"
@@ -470,7 +483,7 @@
</div> </div>
</template> </template>
<div v-if="cardInfo.currentPackageDetail"> <div v-if="cardInfo.currentPackageDetail">
<ElDescriptions :column="3" border> <ElDescriptions :column="4" border>
<!-- 套餐基本信息 --> <!-- 套餐基本信息 -->
<ElDescriptionsItem label="套餐名称" :span="1">{{ <ElDescriptionsItem label="套餐名称" :span="1">{{
cardInfo.currentPackageDetail.package_name || '--' cardInfo.currentPackageDetail.package_name || '--'
@@ -524,11 +537,6 @@
<ElDescriptionsItem label="创建时间">{{ <ElDescriptionsItem label="创建时间">{{
formatDateTime(cardInfo.currentPackageDetail.created_at) formatDateTime(cardInfo.currentPackageDetail.created_at)
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
<!-- 其他信息 -->
<ElDescriptionsItem label="主套餐ID" :span="2">{{
cardInfo.currentPackageDetail.master_usage_id || '--'
}}</ElDescriptionsItem>
</ElDescriptions> </ElDescriptions>
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
@@ -573,40 +581,10 @@
</ElCard> </ElCard>
</div> </div>
<!-- 第三行归属信息和钱包概况 --> <!-- 第三行钱包概况和套餐列表 -->
<div class="row two-columns"> <div class="row two-columns wallet-package-row">
<!-- 左侧归属信息 --> <!-- 左侧钱包概况 -->
<div class="col"> <div class="col wallet-col">
<ElCard shadow="never" class="info-card business-info">
<template #header>
<div class="card-header">
<span>归属信息</span>
</div>
</template>
<ElDescriptions :column="2" border>
<!--<ElDescriptionsItem label="所属店铺">{{-->
<!-- cardInfo?.shop_name || '&#45;&#45;'-->
<!--}}</ElDescriptionsItem>-->
<ElDescriptionsItem label="套餐系列">{{
cardInfo?.series_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{
cardInfo?.batch_no || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(cardInfo?.created_at)
}}</ElDescriptionsItem>
<!--<template v-if="cardInfo?.asset_type === 'card'">-->
<!-- <ElDescriptionsItem label="供应商">{{-->
<!-- cardInfo?.supplier || '&#45;&#45;'-->
<!-- }}</ElDescriptionsItem>-->
<!--</template>-->
</ElDescriptions>
</ElCard>
</div>
<!-- 右侧钱包概况 -->
<div class="col">
<ElCard shadow="never" class="info-card wallet-overview-card"> <ElCard shadow="never" class="info-card wallet-overview-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
@@ -647,28 +625,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 钱包详细信息 -->
<ElDescriptions :column="2" border style="margin-top: 20px">
<ElDescriptionsItem label="钱包ID">{{
walletInfo.wallet_id || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="币种">{{
walletInfo.currency || 'CNY'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="资源类型">{{
walletInfo.resource_type || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="资源ID">{{
walletInfo.resource_id || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(walletInfo.created_at)
}}</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">{{
formatDateTime(walletInfo.updated_at)
}}</ElDescriptionsItem>
</ElDescriptions>
</div> </div>
<ElEmpty <ElEmpty
v-else-if="!walletLoading" v-else-if="!walletLoading"
@@ -677,123 +633,129 @@
/> />
</ElCard> </ElCard>
</div> </div>
</div>
<!-- 套餐列表 --> <!-- 右侧套餐列表 -->
<div class="row full-width"> <div class="col package-col">
<ElCard shadow="never" class="info-card package-info"> <ElCard shadow="never" class="info-card package-info">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>套餐列表</span> <span>套餐列表</span>
</div> </div>
</template> </template>
<div class="package-table-wrapper"> <div class="package-table-wrapper">
<ElTable <ElTable
v-if="cardInfo.packageList && cardInfo.packageList.length > 0" v-if="cardInfo.packageList && cardInfo.packageList.length > 0"
:data="cardInfo.packageList" :data="cardInfo.packageList"
class="package-table" class="package-table"
border border
> max-height="400"
<ElTableColumn label="套餐名称" min-width="150">
<template #default="scope">
<ElButton
type="primary"
link
class="package-name-link"
@click="handleShowDailyRecords(scope.row)"
v-permission="'package-usage:daily-records:view'"
>
{{ scope.row.package_name }}
</ElButton>
</template>
</ElTableColumn>
<ElTableColumn prop="package_type" label="套餐类型" width="100">
<template #default="scope">
<ElTag
:type="scope.row.package_type === 'formal' ? 'primary' : 'warning'"
size="small"
>
{{ scope.row.package_type === 'formal' ? '正式套餐' : '加油包' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="usage_type" label="使用类型" width="110">
<template #default="scope">
<ElTag
:type="scope.row.usage_type === 'single_card' ? 'success' : 'info'"
size="small"
>
{{ scope.row.usage_type === 'single_card' ? '单卡' : '设备' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="status_name" label="状态" width="100">
<template #default="scope">
<ElTag :type="getPackageStatusTagType(scope.row.status)" size="small">
{{ scope.row.status_name }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="data_limit_mb" label="真流量总量" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.data_limit_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="data_usage_mb" label="真流量已用" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.data_usage_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_limit_mb" label="虚流量总量" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.virtual_limit_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_used_mb" label="虚流量已用" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.virtual_used_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_remain_mb" label="虚流量剩余" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.virtual_remain_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_ratio" label="虚流量比例" width="100">
<template #default="scope">
{{ scope.row.virtual_ratio?.toFixed(2) || '1.00' }}
</template>
</ElTableColumn>
<ElTableColumn prop="priority" label="优先级" width="80" />
<ElTableColumn prop="master_usage_id" label="主套餐ID" width="100">
<template #default="scope">
{{ scope.row.master_usage_id || '-' }}
</template>
</ElTableColumn>
<ElTableColumn
prop="activated_at"
label="激活时间"
width="170"
show-overflow-tooltip
> >
<template #default="scope"> <ElTableColumn label="套餐名称" min-width="150">
{{ formatDateTime(scope.row.activated_at) }} <template #default="scope">
</template> <ElButton
</ElTableColumn> type="primary"
<ElTableColumn prop="expires_at" label="到期时间" width="170" show-overflow-tooltip> link
<template #default="scope"> class="package-name-link"
{{ formatDateTime(scope.row.expires_at) }} @click="handleShowDailyRecords(scope.row)"
</template> v-permission="'package-usage:daily-records:view'"
</ElTableColumn> >
<ElTableColumn prop="created_at" label="创建时间" width="170" show-overflow-tooltip> {{ scope.row.package_name }}
<template #default="scope"> </ElButton>
{{ formatDateTime(scope.row.created_at) }} </template>
</template> </ElTableColumn>
</ElTableColumn> <ElTableColumn prop="package_type" label="套餐类型" width="100">
</ElTable> <template #default="scope">
<ElEmpty v-else description="暂无套餐数据" :image-size="100" /> <ElTag
</div> :type="scope.row.package_type === 'formal' ? 'primary' : 'warning'"
</ElCard> size="small"
>
{{ scope.row.package_type === 'formal' ? '正式套餐' : '加油包' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="usage_type" label="使用类型" width="110">
<template #default="scope">
<ElTag
:type="scope.row.usage_type === 'single_card' ? 'success' : 'info'"
size="small"
>
{{ scope.row.usage_type === 'single_card' ? '单卡' : '设备' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="status_name" label="状态" width="100">
<template #default="scope">
<ElTag :type="getPackageStatusTagType(scope.row.status)" size="small">
{{ scope.row.status_name }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="data_limit_mb" label="真流量总量" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.data_limit_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="data_usage_mb" label="真流量已用" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.data_usage_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_limit_mb" label="虚流量总量" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.virtual_limit_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_used_mb" label="虚流量已用" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.virtual_used_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_remain_mb" label="虚流量剩余" width="120">
<template #default="scope">
{{ formatDataSize(scope.row.virtual_remain_mb) }}
</template>
</ElTableColumn>
<ElTableColumn prop="virtual_ratio" label="虚流量比例" width="100">
<template #default="scope">
{{ scope.row.virtual_ratio?.toFixed(2) || '1.00' }}
</template>
</ElTableColumn>
<ElTableColumn prop="priority" label="优先级" width="80" />
<ElTableColumn
prop="activated_at"
label="激活时间"
width="170"
show-overflow-tooltip
>
<template #default="scope">
{{ formatDateTime(scope.row.activated_at) }}
</template>
</ElTableColumn>
<ElTableColumn
prop="expires_at"
label="到期时间"
width="170"
show-overflow-tooltip
>
<template #default="scope">
{{ formatDateTime(scope.row.expires_at) }}
</template>
</ElTableColumn>
<ElTableColumn
prop="created_at"
label="创建时间"
width="170"
show-overflow-tooltip
>
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</ElTableColumn>
</ElTable>
<ElEmpty v-else description="暂无套餐数据" :image-size="100" />
</div>
</ElCard>
</div>
</div> </div>
<!-- 第六行钱包流水列表 --> <!-- 第六行钱包流水列表 -->
@@ -955,7 +917,7 @@
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
/> />
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div> <div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
</ElFormItem> </ElFormItem>
<ElFormItem label="上行速率" prop="upload_speed"> <ElFormItem label="上行速率" prop="upload_speed">
<ElInputNumber <ElInputNumber
@@ -965,7 +927,7 @@
controls-position="right" controls-position="right"
style="width: 100%" style="width: 100%"
/> />
<div style="color: #909399; font-size: 12px; margin-top: 4px">单位: KB/s</div> <div style=" margin-top: 4px; font-size: 12px;color: #909399">单位: KB/s</div>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
<template #footer> <template #footer>
@@ -982,7 +944,7 @@
<!-- 切换SIM卡对话框 --> <!-- 切换SIM卡对话框 -->
<ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px"> <ElDialog v-model="switchCardDialogVisible" title="切换SIM卡" width="600px">
<div v-if="loadingDeviceCards" style="text-align: center; padding: 40px"> <div v-if="loadingDeviceCards" style=" padding: 40px;text-align: center">
<ElIcon class="is-loading" :size="40"><Loading /></ElIcon> <ElIcon class="is-loading" :size="40"><Loading /></ElIcon>
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div> <div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
</div> </div>
@@ -1026,7 +988,7 @@
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`" :label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
:value="card.iccid" :value="card.iccid"
> >
<div style="display: flex; justify-content: space-between; align-items: center"> <div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ card.iccid }}</span> <span>{{ card.iccid }}</span>
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag> <ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
</div> </div>
@@ -1117,7 +1079,7 @@
</ElDescriptions> </ElDescriptions>
<!-- 筛选器 --> <!-- 筛选器 -->
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px"> <div style=" display: flex; gap: 12px; align-items: center;margin-bottom: 16px">
<span style="font-weight: 500">筛选方式</span> <span style="font-weight: 500">筛选方式</span>
<ElRadioGroup v-model="dateFilterType" @change="handleDateFilterChange"> <ElRadioGroup v-model="dateFilterType" @change="handleDateFilterChange">
<ElRadio value="all">全部</ElRadio> <ElRadio value="all">全部</ElRadio>
@@ -1183,7 +1145,7 @@
</ElTableColumn> </ElTableColumn>
</ElTable> </ElTable>
</div> </div>
<div v-else-if="!dailyRecordsLoading" style="text-align: center; padding: 40px 0"> <div v-else-if="!dailyRecordsLoading" style=" padding: 40px 0;text-align: center">
<ElEmpty description="暂无流量详单数据" /> <ElEmpty description="暂无流量详单数据" />
</div> </div>
</ElDialog> </ElDialog>
@@ -1221,7 +1183,7 @@
ElInput ElInput
} from 'element-plus' } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { Loading } from '@element-plus/icons-vue' import { Loading, Search, Refresh } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { EnterpriseService } from '@/api/modules/enterprise' import { EnterpriseService } from '@/api/modules/enterprise'
import { CardService, AssetService, DeviceService } from '@/api/modules' import { CardService, AssetService, DeviceService } from '@/api/modules'
@@ -1773,6 +1735,50 @@
} }
} }
// 计算信号强度等级基于RSSI、RSRP、RSRQ
const calculateSignalStrength = () => {
if (!deviceRealtime.value) return '--'
const rssi = deviceRealtime.value.rssi ? Number(deviceRealtime.value.rssi) : null
const rsrp = deviceRealtime.value.rsrp ? Number(deviceRealtime.value.rsrp) : null
const rsrq = deviceRealtime.value.rsrq ? Number(deviceRealtime.value.rsrq) : null
// 优先使用RSRP判断更准确
// RSRP范围通常在-140到-44 dBm之间0或正值表示无效数据
if (rsrp !== null && !isNaN(rsrp) && rsrp < 0) {
if (rsrp >= -80) return '优秀'
if (rsrp >= -90) return '良好'
if (rsrp >= -100) return '一般'
if (rsrp >= -110) return '较差'
return '极差'
}
// 其次使用RSSI
// RSSI范围通常在-113到-51 dBm之间0或正值表示无效数据
if (rssi !== null && !isNaN(rssi) && rssi < 0) {
if (rssi >= -70) return '优秀'
if (rssi >= -85) return '良好'
if (rssi >= -100) return '一般'
if (rssi >= -110) return '较差'
return '极差'
}
return '--'
}
// 获取信号强度标签类型
const getSignalStrengthType = () => {
const strength = calculateSignalStrength()
const map: Record<string, string> = {
优秀: 'success',
良好: 'success',
一般: 'warning',
较差: 'danger',
极差: 'danger'
}
return map[strength] || 'info'
}
// 点击ICCID查看卡详情 // 点击ICCID查看卡详情
const handleViewCardDetail = (iccid: string) => { const handleViewCardDetail = (iccid: string) => {
if (!iccid) return if (!iccid) return
@@ -2295,7 +2301,7 @@
}) })
enableCardLoading.value = true enableCardLoading.value = true
const res = await CardService.enableIotCard(cardInfo.value.asset_id) const res = await CardService.enableIotCard(cardInfo.value.iccid)
if (res.code === 0) { if (res.code === 0) {
ElMessage.success('启用成功') ElMessage.success('启用成功')
await handleRefreshAsset() await handleRefreshAsset()
@@ -2318,7 +2324,7 @@
}) })
disableCardLoading.value = true disableCardLoading.value = true
const res = await CardService.disableIotCard(cardInfo.value.asset_id) const res = await CardService.disableIotCard(cardInfo.value.iccid)
if (res.code === 0) { if (res.code === 0) {
ElMessage.success('停用成功') ElMessage.success('停用成功')
await handleRefreshAsset() await handleRefreshAsset()
@@ -2380,7 +2386,6 @@
} catch (error: any) { } catch (error: any) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('重启失败:', error) console.error('重启失败:', error)
ElMessage.error(error?.message || '重启失败')
} }
} finally { } finally {
rebootDeviceLoading.value = false rebootDeviceLoading.value = false
@@ -2405,7 +2410,6 @@
} catch (error: any) { } catch (error: any) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('恢复出厂失败:', error) console.error('恢复出厂失败:', error)
ElMessage.error(error?.message || '恢复出厂失败')
} }
} finally { } finally {
resetDeviceLoading.value = false resetDeviceLoading.value = false
@@ -2560,7 +2564,6 @@
} catch (error: any) { } catch (error: any) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('手动停用失败:', error) console.error('手动停用失败:', error)
ElMessage.error(error?.message || '手动停用失败')
} }
} finally { } finally {
manualDeactivateDeviceLoading.value = false manualDeactivateDeviceLoading.value = false
@@ -2672,9 +2675,9 @@
.iccid-search { .iccid-search {
display: flex; display: flex;
align-items: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
align-items: flex-start;
overflow: visible; overflow: visible;
.iccid-input-wrapper { .iccid-input-wrapper {
@@ -2686,8 +2689,8 @@
position: absolute; position: absolute;
top: calc(100% + 12px); top: calc(100% + 12px);
left: 0; left: 0;
white-space: nowrap;
z-index: 1000; z-index: 1000;
white-space: nowrap;
// 三角箭头 - 上边框样式 // 三角箭头 - 上边框样式
.magnifier-arrow { .magnifier-arrow {
@@ -2706,6 +2709,7 @@
.magnifier-content { .magnifier-content {
display: inline-block; display: inline-block;
padding: 16px 24px; padding: 16px 24px;
font-family: 'Courier New', Courier, monospace;
font-size: 28px; font-size: 28px;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
@@ -2716,8 +2720,7 @@
background-color: var(--el-bg-color, #fff); background-color: var(--el-bg-color, #fff);
border: 2px solid var(--el-color-primary); border: 2px solid var(--el-color-primary);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
font-family: 'Courier New', Courier, monospace;
} }
} }
} }
@@ -2738,7 +2741,7 @@
} }
// 移动端 // 移动端
@media (max-width: 768px) { @media (width <= 768px) {
padding: 16px; padding: 16px;
.search-card { .search-card {
@@ -2827,6 +2830,17 @@
} }
} }
} }
// 钱包和套餐两栏在小屏幕也改为垂直布局
.wallet-package-row {
flex-direction: column;
.wallet-col,
.package-col {
flex: 1;
min-width: 100%;
}
}
} }
@media (width <= 768px) { @media (width <= 768px) {
@@ -2858,8 +2872,8 @@
} }
.package-series-value { .package-series-value {
color: var(--el-color-primary);
font-weight: 500; font-weight: 500;
color: var(--el-color-primary);
} }
i { i {
@@ -2882,8 +2896,8 @@
} }
.traffic-progress-wrapper { .traffic-progress-wrapper {
margin-top: 24px;
padding: 20px; padding: 20px;
margin-top: 24px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-radius: 8px; border-radius: 8px;
@@ -2908,19 +2922,19 @@
.flow-stats { .flow-stats {
display: flex; display: flex;
justify-content: space-between;
gap: 12px; gap: 12px;
justify-content: space-between;
margin-bottom: 12px; margin-bottom: 12px;
.stat-item { .stat-item {
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
flex: 1;
padding: 12px; padding: 12px;
text-align: center;
background: var(--el-bg-color, #fff); background: var(--el-bg-color, #fff);
border-radius: 6px; border-radius: 6px;
text-align: center;
.stat-label { .stat-label {
font-size: 12px; font-size: 12px;
@@ -2987,19 +3001,19 @@
.progress-stats { .progress-stats {
display: flex; display: flex;
justify-content: space-between;
gap: 12px; gap: 12px;
justify-content: space-between;
margin-top: 8px; margin-top: 8px;
.stat-item { .stat-item {
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
flex: 1;
padding: 12px; padding: 12px;
text-align: center;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-radius: 6px; border-radius: 6px;
text-align: center;
.stat-label { .stat-label {
font-size: 12px; font-size: 12px;
@@ -3031,13 +3045,13 @@
.info-item { .info-item {
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
flex: 1;
padding: 12px; padding: 12px;
text-align: center;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-radius: 8px; border-radius: 8px;
text-align: center;
.label { .label {
font-size: 13px; font-size: 13px;
@@ -3054,9 +3068,41 @@
} }
} }
// 钱包概况和套餐列表两栏布局
.wallet-package-row {
align-items: stretch;
.wallet-col {
flex: 0 0 380px;
min-width: 320px;
max-width: 400px;
}
.package-col {
flex: 1;
min-width: 0;
}
.info-card {
display: flex;
flex-direction: column;
height: 100%;
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
}
}
// 套餐信息卡片 // 套餐信息卡片
.package-info { .package-info {
.package-table-wrapper { .package-table-wrapper {
flex: 1;
overflow: auto;
.package-table { .package-table {
:deep(.el-table__header) { :deep(.el-table__header) {
th { th {
@@ -3073,66 +3119,66 @@
.wallet-balance-cards { .wallet-balance-cards {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 14px;
margin-bottom: 20px;
.balance-card { .balance-card {
display: flex; display: flex;
gap: 18px;
align-items: center; align-items: center;
gap: 16px; padding: 18px 20px;
padding: 16px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px; border-radius: 8px;
transition: all 0.3s ease; transition: all 0.3s ease;
&:hover { &:hover {
transform: translateY(-2px); border-color: var(--el-color-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
transform: translateX(4px);
} }
.balance-icon { .balance-icon {
font-size: 28px; flex-shrink: 0;
font-size: 32px;
line-height: 1; line-height: 1;
} }
.balance-info { .balance-info {
flex: 1;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 6px;
.balance-label { .balance-label {
font-size: 12px; font-size: 13px;
font-weight: 500;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
} }
.balance-value { .balance-value {
font-size: 20px; font-size: 22px;
font-weight: 700; font-weight: 700;
line-height: 1.2;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }
} }
&.total { &.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.balance-label,
.balance-value { .balance-value {
color: #fff; color: var(--el-color-primary);
} }
} }
&.available { &.available {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
.balance-label,
.balance-value { .balance-value {
color: #fff; color: var(--el-color-success);
} }
} }
&.frozen { &.frozen {
background: var(--el-fill-color-light); .balance-value {
color: var(--el-color-warning);
}
} }
} }
} }
@@ -3142,10 +3188,10 @@
.wallet-info { .wallet-info {
.wallet-header { .wallet-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
.header-left { .header-left {
display: flex; display: flex;
@@ -3155,9 +3201,9 @@
.filter-section { .filter-section {
display: flex; display: flex;
flex-wrap: wrap;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
} }
@@ -3172,10 +3218,10 @@
.reference-no-link { .reference-no-link {
color: var(--el-color-primary); color: var(--el-color-primary);
cursor: pointer;
text-decoration: underline; text-decoration: underline;
text-decoration-style: dashed; text-decoration-style: dashed;
text-underline-offset: 2px; text-underline-offset: 2px;
cursor: pointer;
&:hover { &:hover {
color: var(--el-color-primary-light-3); color: var(--el-color-primary-light-3);

View File

@@ -139,7 +139,8 @@
{ {
label: '实付金额', label: '实付金额',
prop: 'actual_paid_amount', prop: 'actual_paid_amount',
formatter: (value) => (value !== undefined && value !== null ? formatCurrency(value) : '-') formatter: (value) =>
value !== undefined && value !== null ? formatCurrency(value) : '-'
}, },
{ {
label: 'IoT卡ID', label: 'IoT卡ID',
@@ -157,7 +158,8 @@
}, },
{ {
label: '订单渠道', label: '订单渠道',
formatter: (_, data) => (data.purchase_role ? getPurchaseRoleText(data.purchase_role) : '-') formatter: (_, data) =>
data.purchase_role ? getPurchaseRoleText(data.purchase_role) : '-'
}, },
{ {
label: '购买备注', label: '购买备注',
@@ -256,8 +258,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -269,12 +271,12 @@
} }
.order-items-section { .order-items-section {
margin-top: 30px;
padding-top: 20px; padding-top: 20px;
margin-top: 30px;
border-top: 1px solid var(--el-border-color-lighter); border-top: 1px solid var(--el-border-color-lighter);
.section-title { .section-title {
margin: 0 0 16px 0; margin: 0 0 16px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
@@ -284,10 +286,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -99,7 +99,7 @@
> >
<div style="display: flex; justify-content: space-between"> <div style="display: flex; justify-content: space-between">
<span>{{ card.iccid }}</span> <span>{{ card.iccid }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px"> <span style=" font-size: 12px;color: var(--el-text-color-secondary)">
{{ card.msisdn || '无接入号' }} {{ card.msisdn || '无接入号' }}
</span> </span>
</div> </div>
@@ -130,7 +130,7 @@
> >
<div style="display: flex; justify-content: space-between"> <div style="display: flex; justify-content: space-between">
<span>{{ device.virtual_no }}</span> <span>{{ device.virtual_no }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px"> <span style=" font-size: 12px;color: var(--el-text-color-secondary)">
{{ device.device_name }} {{ device.device_name }}
</span> </span>
</div> </div>
@@ -163,7 +163,7 @@
> >
<div style="display: flex; justify-content: space-between"> <div style="display: flex; justify-content: space-between">
<span>{{ pkg.package_name }}</span> <span>{{ pkg.package_name }}</span>
<span style="color: var(--el-text-color-secondary); font-size: 12px"> <span style=" font-size: 12px;color: var(--el-text-color-secondary)">
¥{{ (pkg.cost_price / 100).toFixed(2) }} ¥{{ (pkg.cost_price / 100).toFixed(2) }}
</span> </span>
</div> </div>

View File

@@ -259,8 +259,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -274,10 +274,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -61,11 +61,11 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增套餐' : '编辑套餐'" :title="dialogType === 'add' ? '新增套餐' : '编辑套餐'"
width="60%" width="45%"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px"> <ElForm ref="formRef" :model="form" :rules="rules" label-width="150px">
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="24"> <ElCol :span="24">
<ElFormItem label="套餐编码" prop="package_code"> <ElFormItem label="套餐编码" prop="package_code">
@@ -109,7 +109,7 @@
:value="series.id" :value="series.id"
/> />
<template #empty> <template #empty>
<div style="padding: 10px; text-align: center; color: #909399"> <div style="padding: 10px; color: #909399; text-align: center">
暂无套餐系列请先创建套餐系列 暂无套餐系列请先创建套餐系列
</div> </div>
</template> </template>
@@ -182,17 +182,17 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<!-- 流量重置天数calendar_type为by_day时显示 --> <!-- 套餐天数流量重置周期为每日calendar_type为by_day时显示 -->
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'"> <ElRow :gutter="20" v-if="form.data_reset_cycle === 'daily' || form.calendar_type === 'by_day'">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="流量重置天数" prop="duration_days"> <ElFormItem label="套餐天数" prop="duration_days">
<ElInputNumber <ElInputNumber
v-model="form.duration_days" v-model="form.duration_days"
:min="1" :min="1"
:max="3650" :max="3650"
:controls="false" :controls="false"
style="width: 100%" style="width: 100%"
placeholder="请输入流量重置天数" placeholder="请输入套餐天数"
/> />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
@@ -341,7 +341,11 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<ElButton @click="retailPriceDialogVisible = false">取消</ElButton> <ElButton @click="retailPriceDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmUpdateRetailPrice" :loading="retailPriceLoading"> <ElButton
type="primary"
@click="handleConfirmUpdateRetailPrice"
:loading="retailPriceLoading"
>
确认修改 确认修改
</ElButton> </ElButton>
</div> </div>
@@ -547,13 +551,25 @@
cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }] cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }]
} }
// 如果启用虚流量,则虚流量额度为必填 // 如果启用虚流量,则虚流量额度为必填,并且不能超过真流量额度
if (form.enable_virtual_data) { if (form.enable_virtual_data) {
baseRules.virtual_data_mb = [{ required: true, message: '请输入虚流量额度', trigger: 'blur' }] baseRules.virtual_data_mb = [
{ required: true, message: '请输入虚流量额度', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value && form.real_data_mb && value > form.real_data_mb) {
callback(new Error('虚流量额度不能超过真流量额度'))
} else {
callback()
}
},
trigger: 'blur'
}
]
} }
// 如果套餐周期类型是按天,则套餐天数为必填 // 如果流量重置周期是每日,或套餐周期类型是按天,则套餐天数为必填
if (form.calendar_type === 'by_day') { if (form.data_reset_cycle === 'daily' || form.calendar_type === 'by_day') {
baseRules.duration_days = [ baseRules.duration_days = [
{ required: true, message: '请输入套餐天数', trigger: 'blur' }, { required: true, message: '请输入套餐天数', trigger: 'blur' },
{ type: 'number', min: 1, max: 3650, message: '套餐天数范围 1-3650 天', trigger: 'blur' } { type: 'number', min: 1, max: 3650, message: '套餐天数范围 1-3650 天', trigger: 'blur' }
@@ -755,22 +771,36 @@
} }
) )
// 监听流量重置周期变化,不是每月时清空套餐周期类型 // 监听流量额度变化,如果虚流量超过真流量则触发验证
watch(
() => form.real_data_mb,
() => {
if (form.enable_virtual_data && formRef.value) {
formRef.value.validateField('virtual_data_mb')
}
}
)
// 监听流量重置周期变化
watch( watch(
() => form.data_reset_cycle, () => form.data_reset_cycle,
(cycle) => { (cycle) => {
// 不是每月时清空套餐周期类型
if (cycle !== 'monthly') { if (cycle !== 'monthly') {
form.calendar_type = undefined form.calendar_type = undefined
}
// 不是每日且calendar_type不是按天时清空套餐天数
if (cycle !== 'daily' && form.calendar_type !== 'by_day') {
form.duration_days = undefined form.duration_days = undefined
} }
} }
) )
// 监听套餐周期类型变化,不是按天时清空套餐天数 // 监听套餐周期类型变化,不是按天且流量重置周期不是每日时清空套餐天数
watch( watch(
() => form.calendar_type, () => form.calendar_type,
(type) => { (type) => {
if (type !== 'by_day') { if (type !== 'by_day' && form.data_reset_cycle !== 'daily') {
form.duration_days = undefined form.duration_days = undefined
} }
} }
@@ -1036,7 +1066,8 @@
if (form.data_reset_cycle === 'monthly' && form.calendar_type) { if (form.data_reset_cycle === 'monthly' && form.calendar_type) {
data.calendar_type = form.calendar_type data.calendar_type = form.calendar_type
} }
if (form.calendar_type === 'by_day' && form.duration_days) { // 当流量重置周期为每日,或套餐周期类型为按天时,传递套餐天数
if ((form.data_reset_cycle === 'daily' || form.calendar_type === 'by_day') && form.duration_days) {
data.duration_days = form.duration_days data.duration_days = form.duration_days
} }
if (suggestedRetailPriceInCents !== undefined) { if (suggestedRetailPriceInCents !== undefined) {

View File

@@ -244,8 +244,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -259,10 +259,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -597,8 +597,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
margin-bottom: 20px; margin-bottom: 20px;
border-bottom: 1px solid var(--el-border-color); border-bottom: 1px solid var(--el-border-color);
@@ -619,8 +619,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 8px; padding-bottom: 8px;
margin-bottom: 12px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
@@ -642,10 +642,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -211,7 +211,7 @@
<ElDialog <ElDialog
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'" :title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'"
width="60%" width="45%"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
@@ -2184,8 +2184,8 @@
} }
.form-section-title { .form-section-title {
margin: 20px 0 16px;
padding-bottom: 8px; padding-bottom: 8px;
margin: 20px 0 16px;
border-bottom: 1px solid var(--el-border-color); border-bottom: 1px solid var(--el-border-color);
.title-text { .title-text {
@@ -2197,8 +2197,8 @@
.commission-type-display { .commission-type-display {
display: flex; display: flex;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
.type-hint { .type-hint {
font-size: 13px; font-size: 13px;
@@ -2252,8 +2252,8 @@
.cost-price-input-wrapper { .cost-price-input-wrapper {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
align-items: center;
.min-cost-hint { .min-cost-hint {
font-size: 12px; font-size: 12px;
@@ -2282,8 +2282,8 @@
.info-label { .info-label {
margin-right: 8px; margin-right: 8px;
font-size: 14px; font-size: 14px;
white-space: nowrap;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
white-space: nowrap;
} }
.info-value { .info-value {
@@ -2294,8 +2294,8 @@
.readonly-value { .readonly-value {
font-size: 14px; font-size: 14px;
color: var(--el-text-color-primary);
font-weight: 500; font-weight: 500;
color: var(--el-text-color-primary);
} }
.package-list-dialog-content { .package-list-dialog-content {

View File

@@ -285,8 +285,8 @@
<style scoped lang="scss"> <style scoped lang="scss">
.alert-history-page { .alert-history-page {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
} }
</style> </style>

View File

@@ -58,7 +58,11 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="告警级别" prop="alert_level"> <el-form-item label="告警级别" prop="alert_level">
<el-select v-model="form.alert_level" placeholder="请选择告警级别" style="width: 100%"> <el-select
v-model="form.alert_level"
placeholder="请选择告警级别"
style="width: 100%"
>
<el-option label="警告" value="warning" /> <el-option label="警告" value="warning" />
<el-option label="严重" value="critical" /> <el-option label="严重" value="critical" />
</el-select> </el-select>
@@ -155,188 +159,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, nextTick, watch, h } from 'vue' import { ref, reactive, computed, nextTick, watch, h } from 'vue'
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
import { PollingAlertService } from '@/api/modules' import { PollingAlertService } from '@/api/modules'
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api' import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu' import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
const loading = ref(false) const loading = ref(false)
const tableData = ref<PollingAlertRule[]>([]) const tableData = ref<PollingAlertRule[]>([])
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogType = ref<'create' | 'edit'>('create') const dialogType = ref<'create' | 'edit'>('create')
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const contextMenuRef = ref() const contextMenuRef = ref()
const currentRow = ref<PollingAlertRule | null>(null) const currentRow = ref<PollingAlertRule | null>(null)
// 使用表格右键菜单功能 // 使用表格右键菜单功能
const { const {
showContextMenuHint, showContextMenuHint,
hintPosition, hintPosition,
getRowClassName, getRowClassName,
handleCellMouseEnter, handleCellMouseEnter,
handleCellMouseLeave handleCellMouseLeave
} = useTableContextMenu() } = useTableContextMenu()
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({ const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
rule_name: '',
alert_level: '',
metric_type: '',
task_type: '',
threshold: 0,
operator: '>',
cooldown_minutes: 5,
notify_channels: ''
})
const rules: FormRules = {
rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
}
const getAlertLevelLabel = (level: string) => {
return level === 'critical' ? '严重' : '警告'
}
const getMetricTypeLabel = (type: string) => {
const labels: Record<string, string> = {
queue_size: '队列大小',
success_rate: '成功率',
avg_duration: '平均执行时间',
concurrency: '并发数'
}
return labels[type] || type
}
const getTaskTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'polling:realname': '实名认证轮询',
'polling:carddata': '卡数据轮询',
'polling:package': '套餐轮询'
}
return labels[type] || type
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'rule_name',
label: '规则名称',
minWidth: 150
},
{
prop: 'alert_level',
label: '告警级别',
width: 100,
slots: {
default: ({ row }: any) =>
h(
ElTag,
{ type: row.alert_level === 'critical' ? 'danger' : 'warning' },
() => getAlertLevelLabel(row.alert_level)
)
}
},
{
prop: 'metric_type_name',
label: '指标类型',
width: 120,
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
},
{
prop: 'task_type_name',
label: '任务类型',
width: 150,
formatter: (row: any) => getTaskTypeLabel(row.task_type)
},
{
prop: 'operator',
label: '运算符',
width: 80
},
{
prop: 'threshold',
label: '阈值',
width: 100
},
{
prop: 'cooldown_minutes',
label: '冷却时间(分钟)',
width: 120
},
{
prop: 'status',
label: '状态',
width: 80,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
row.status === 1 ? '启用' : '禁用'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
// 右键菜单项
const contextMenuItems = computed<MenuItemType[]>(() => {
return [
{ key: 'edit', label: '编辑' },
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
{ key: 'delete', label: '删除' }
]
})
watch(dialogVisible, (val) => {
if (!val) {
nextTick(() => {
formRef.value?.clearValidate()
})
}
})
const loadData = async () => {
loading.value = true
try {
const res = await PollingAlertService.getPollingAlertRules()
if (res.code === 0 && res.data) {
tableData.value = res.data.items
}
} catch (error) {
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
loadData()
}
const resetForm = () => {
Object.assign(form, {
id: undefined,
rule_name: '', rule_name: '',
alert_level: '', alert_level: '',
metric_type: '', metric_type: '',
@@ -346,134 +195,287 @@ const resetForm = () => {
cooldown_minutes: 5, cooldown_minutes: 5,
notify_channels: '' notify_channels: ''
}) })
}
const showCreateDialog = () => { const rules: FormRules = {
resetForm() rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
dialogType.value = 'create' alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
dialogVisible.value = true metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
nextTick(() => { task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
formRef.value?.clearValidate() threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
}
const getAlertLevelLabel = (level: string) => {
return level === 'critical' ? '严重' : '警告'
}
const getMetricTypeLabel = (type: string) => {
const labels: Record<string, string> = {
queue_size: '队列大小',
success_rate: '成功率',
avg_duration: '平均执行时间',
concurrency: '并发数'
}
return labels[type] || type
}
const getTaskTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'polling:realname': '实名认证轮询',
'polling:carddata': '卡数据轮询',
'polling:package': '套餐轮询'
}
return labels[type] || type
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'rule_name',
label: '规则名称',
minWidth: 150
},
{
prop: 'alert_level',
label: '告警级别',
width: 100,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: row.alert_level === 'critical' ? 'danger' : 'warning' }, () =>
getAlertLevelLabel(row.alert_level)
)
}
},
{
prop: 'metric_type_name',
label: '指标类型',
width: 120,
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
},
{
prop: 'task_type_name',
label: '任务类型',
width: 150,
formatter: (row: any) => getTaskTypeLabel(row.task_type)
},
{
prop: 'operator',
label: '运算符',
width: 80
},
{
prop: 'threshold',
label: '阈值',
width: 100
},
{
prop: 'cooldown_minutes',
label: '冷却时间(分钟)',
width: 120
},
{
prop: 'status',
label: '状态',
width: 80,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
row.status === 1 ? '启用' : '禁用'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
// 右键菜单项
const contextMenuItems = computed<MenuItemType[]>(() => {
return [
{ key: 'edit', label: '编辑' },
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
{ key: 'delete', label: '删除' }
]
}) })
}
const showEditDialog = async (row: PollingAlertRule) => { watch(dialogVisible, (val) => {
try { if (!val) {
const res = await PollingAlertService.getPollingAlertRuleById(row.id)
if (res.code === 0 && res.data) {
Object.assign(form, {
id: res.data.id,
rule_name: res.data.rule_name,
alert_level: res.data.alert_level,
metric_type: res.data.metric_type,
task_type: res.data.task_type,
threshold: res.data.threshold,
operator: res.data.operator,
cooldown_minutes: res.data.cooldown_minutes,
notify_channels: res.data.notify_channels
})
dialogType.value = 'edit'
dialogVisible.value = true
nextTick(() => { nextTick(() => {
formRef.value?.clearValidate() formRef.value?.clearValidate()
}) })
} }
} catch (error) {
ElMessage.error('获取规则详情失败')
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (dialogType.value === 'create') {
const res = await PollingAlertService.createPollingAlertRule(form)
if (res.code === 0) {
ElMessage.success('创建成功')
dialogVisible.value = false
loadData()
}
} else {
const { id, metric_type, task_type, operator, ...updateData } = form
const res = await PollingAlertService.updatePollingAlertRule(id!, updateData)
if (res.code === 0) {
ElMessage.success('更新成功')
dialogVisible.value = false
loadData()
}
}
} catch (error) {
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
}
}
}) })
}
const toggleStatus = async (row: PollingAlertRule) => { const loadData = async () => {
try { loading.value = true
const newStatus = row.status === 1 ? 0 : 1
const res = await PollingAlertService.updatePollingAlertRule(row.id, { status: newStatus })
if (res.code === 0) {
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
loadData()
}
} catch (error) {
ElMessage.error('操作失败')
}
}
const handleDelete = (row: PollingAlertRule) => {
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try { try {
const res = await PollingAlertService.deletePollingAlertRule(row.id) const res = await PollingAlertService.getPollingAlertRules()
if (res.code === 0 && res.data) {
tableData.value = res.data.items
}
} catch (error) {
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
loadData()
}
const resetForm = () => {
Object.assign(form, {
id: undefined,
rule_name: '',
alert_level: '',
metric_type: '',
task_type: '',
threshold: 0,
operator: '>',
cooldown_minutes: 5,
notify_channels: ''
})
}
const showCreateDialog = () => {
resetForm()
dialogType.value = 'create'
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
})
}
const showEditDialog = async (row: PollingAlertRule) => {
try {
const res = await PollingAlertService.getPollingAlertRuleById(row.id)
if (res.code === 0 && res.data) {
Object.assign(form, {
id: res.data.id,
rule_name: res.data.rule_name,
alert_level: res.data.alert_level,
metric_type: res.data.metric_type,
task_type: res.data.task_type,
threshold: res.data.threshold,
operator: res.data.operator,
cooldown_minutes: res.data.cooldown_minutes,
notify_channels: res.data.notify_channels
})
dialogType.value = 'edit'
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
})
}
} catch (error) {
ElMessage.error('获取规则详情失败')
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (dialogType.value === 'create') {
const res = await PollingAlertService.createPollingAlertRule(form)
if (res.code === 0) {
ElMessage.success('创建成功')
dialogVisible.value = false
loadData()
}
} else {
const { id, metric_type, task_type, operator, ...updateData } = form
const res = await PollingAlertService.updatePollingAlertRule(id!, updateData)
if (res.code === 0) {
ElMessage.success('更新成功')
dialogVisible.value = false
loadData()
}
}
} catch (error) {
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
}
}
})
}
const toggleStatus = async (row: PollingAlertRule) => {
try {
const newStatus = row.status === 1 ? 0 : 1
const res = await PollingAlertService.updatePollingAlertRule(row.id, { status: newStatus })
if (res.code === 0) { if (res.code === 0) {
ElMessage.success('删除成功') ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
loadData() loadData()
} }
} catch (error) { } catch (error) {
ElMessage.error('删除失败') ElMessage.error('操作失败')
} }
})
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentRow.value)
break
case 'toggle':
toggleStatus(currentRow.value)
break
case 'delete':
handleDelete(currentRow.value)
break
} }
}
loadData() const handleDelete = (row: PollingAlertRule) => {
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await PollingAlertService.deletePollingAlertRule(row.id)
if (res.code === 0) {
ElMessage.success('删除成功')
loadData()
}
} catch (error) {
ElMessage.error('删除失败')
}
})
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingAlertRule, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentRow.value)
break
case 'toggle':
toggleStatus(currentRow.value)
break
case 'delete':
handleDelete(currentRow.value)
break
}
}
loadData()
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.alert-rules-page { .alert-rules-page {
height: 100%; display: flex;
display: flex; flex-direction: column;
flex-direction: column; height: 100%;
} }
</style> </style>

View File

@@ -71,12 +71,7 @@
width="500px" width="500px"
@close="handleDialogClose" @close="handleDialogClose"
> >
<el-form <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-form-item label="任务类型"> <el-form-item label="任务类型">
<el-input v-model="currentRow.task_type_name" disabled /> <el-input v-model="currentRow.task_type_name" disabled />
</el-form-item> </el-form-item>
@@ -99,194 +94,183 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { PollingConcurrencyService } from '@/api/modules' import { PollingConcurrencyService } from '@/api/modules'
import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api' import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue' import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import { useTableContextMenu } from '@/composables/useTableContextMenu' import { useTableContextMenu } from '@/composables/useTableContextMenu'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
const loading = ref(false) const loading = ref(false)
const tableData = ref<PollingConcurrency[]>([]) const tableData = ref<PollingConcurrency[]>([])
const dialogVisible = ref(false) const dialogVisible = ref(false)
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>() const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentClickRow = ref<PollingConcurrency | null>(null) const currentClickRow = ref<PollingConcurrency | null>(null)
const currentRow = ref<PollingConcurrency>({ const currentRow = ref<PollingConcurrency>({
task_type: '', task_type: '',
task_type_name: '', task_type_name: '',
max_concurrency: 0, max_concurrency: 0,
current: 0, current: 0,
available: 0, available: 0,
utilization: 0 utilization: 0
}) })
const formData = reactive<UpdateConcurrencyParams>({ const formData = reactive<UpdateConcurrencyParams>({
max_concurrency: 100 max_concurrency: 100
}) })
const formRules: FormRules = { const formRules: FormRules = {
max_concurrency: [ max_concurrency: [
{ required: true, message: '请输入最大并发数', trigger: 'blur' }, { required: true, message: '请输入最大并发数', trigger: 'blur' },
{ type: 'number', min: 1, max: 1000, message: '并发数范围为 1-1000', trigger: 'blur' } { type: 'number', min: 1, max: 1000, message: '并发数范围为 1-1000', trigger: 'blur' }
] ]
}
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const contextMenuItems = computed<MenuItemType[]>(() => [
{
label: '修改配置',
key: 'edit',
permission: 'polling_concurrency_update'
},
{
label: '重置',
key: 'reset',
permission: 'polling_concurrency_reset'
} }
])
const getUtilizationColor = (percentage: number) => { const {
if (percentage >= 90) return '#f56c6c' showContextMenuHint,
if (percentage >= 70) return '#e6a23c' hintPosition,
if (percentage >= 50) return '#409eff' getRowClassName,
return '#67c23a' handleCellMouseEnter,
} handleCellMouseLeave
} = useTableContextMenu()
const loadData = async () => { const contextMenuItems = computed<MenuItemType[]>(() => [
loading.value = true {
try { label: '修改配置',
const { data } = await PollingConcurrencyService.getConcurrencyList() key: 'edit',
tableData.value = data.items permission: 'polling_concurrency_update'
} catch (error) { },
ElMessage.error('加载并发配置失败') {
} finally { label: '重置',
loading.value = false key: 'reset',
} permission: 'polling_concurrency_reset'
}
const handleEdit = async (row: PollingConcurrency) => {
try {
// 调用接口#2获取指定任务类型的最新并发配置
const { data } = await PollingConcurrencyService.getConcurrencyByType(row.task_type)
currentRow.value = { ...data }
formData.max_concurrency = data.max_concurrency
dialogVisible.value = true
} catch (error) {
ElMessage.error('获取并发配置失败')
}
}
const handleReset = async (row: PollingConcurrency) => {
try {
await ElMessageBox.confirm(
`确定要重置 ${row.task_type_name} 的并发计数吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await PollingConcurrencyService.resetConcurrency({ task_type: row.task_type })
ElMessage.success('重置成功')
await loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('重置失败')
} }
])
const getUtilizationColor = (percentage: number) => {
if (percentage >= 90) return '#f56c6c'
if (percentage >= 70) return '#e6a23c'
if (percentage >= 50) return '#409eff'
return '#67c23a'
} }
}
const handleResetAll = async () => {
try {
await ElMessageBox.confirm(
'确定要重置所有任务类型的并发计数吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await PollingConcurrencyService.resetConcurrency()
ElMessage.success('重置成功')
await loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('重置失败')
}
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async valid => {
if (!valid) return
const loadData = async () => {
loading.value = true
try { try {
await PollingConcurrencyService.updateConcurrency( const { data } = await PollingConcurrencyService.getConcurrencyList()
currentRow.value.task_type, tableData.value = data.items
formData } catch (error) {
) ElMessage.error('加载并发配置失败')
ElMessage.success('修改成功') } finally {
dialogVisible.value = false loading.value = false
}
}
const handleEdit = async (row: PollingConcurrency) => {
try {
// 调用接口#2获取指定任务类型的最新并发配置
const { data } = await PollingConcurrencyService.getConcurrencyByType(row.task_type)
currentRow.value = { ...data }
formData.max_concurrency = data.max_concurrency
dialogVisible.value = true
} catch (error) {
ElMessage.error('获取并发配置失败')
}
}
const handleReset = async (row: PollingConcurrency) => {
try {
await ElMessageBox.confirm(`确定要重置 ${row.task_type_name} 的并发计数吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await PollingConcurrencyService.resetConcurrency({ task_type: row.task_type })
ElMessage.success('重置成功')
await loadData() await loadData()
} catch (error) { } catch (error) {
ElMessage.error('修改失败') if (error !== 'cancel') {
ElMessage.error('重置失败')
}
} }
})
}
const handleDialogClose = () => {
formRef.value?.resetFields()
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingConcurrency, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentClickRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentClickRow.value) return
switch (item.key) {
case 'edit':
handleEdit(currentClickRow.value)
break
case 'reset':
handleReset(currentClickRow.value)
break
} }
}
onMounted(() => { const handleResetAll = async () => {
loadData() try {
}) await ElMessageBox.confirm('确定要重置所有任务类型的并发计数吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await PollingConcurrencyService.resetConcurrency()
ElMessage.success('重置成功')
await loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('重置失败')
}
}
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
try {
await PollingConcurrencyService.updateConcurrency(currentRow.value.task_type, formData)
ElMessage.success('修改成功')
dialogVisible.value = false
await loadData()
} catch (error) {
ElMessage.error('修改失败')
}
})
}
const handleDialogClose = () => {
formRef.value?.resetFields()
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingConcurrency, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentClickRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentClickRow.value) return
switch (item.key) {
case 'edit':
handleEdit(currentClickRow.value)
break
case 'reset':
handleReset(currentClickRow.value)
break
}
}
onMounted(() => {
loadData()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; align-items: center;
align-items: center; justify-content: space-between;
} }
</style> </style>

View File

@@ -697,8 +697,8 @@
<style scoped lang="scss"> <style scoped lang="scss">
.polling-config-page { .polling-config-page {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
} }
</style> </style>

View File

@@ -208,7 +208,7 @@
:total="logPagination.total" :total="logPagination.total"
:page-sizes="[10, 20, 50]" :page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end" style=" justify-content: flex-end;margin-top: 16px"
@size-change="getCleanupLogs" @size-change="getCleanupLogs"
@current-change="getCleanupLogs" @current-change="getCleanupLogs"
/> />
@@ -619,7 +619,7 @@
} }
.text-danger { .text-danger {
color: var(--el-color-danger);
font-weight: 600; font-weight: 600;
color: var(--el-color-danger);
} }
</style> </style>

View File

@@ -86,7 +86,11 @@
> >
<el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="120px"> <el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="120px">
<el-form-item label="任务类型" prop="task_type"> <el-form-item label="任务类型" prop="task_type">
<el-select v-model="batchForm.task_type" placeholder="请选择任务类型" style="width: 100%"> <el-select
v-model="batchForm.task_type"
placeholder="请选择任务类型"
style="width: 100%"
>
<el-option label="实名认证轮询" value="polling:realname" /> <el-option label="实名认证轮询" value="polling:realname" />
<el-option label="卡数据轮询" value="polling:carddata" /> <el-option label="卡数据轮询" value="polling:carddata" />
<el-option label="套餐轮询" value="polling:package" /> <el-option label="套餐轮询" value="polling:package" />
@@ -261,7 +265,11 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="任务类型" prop="task_type"> <el-form-item label="任务类型" prop="task_type">
<el-select v-model="singleForm.task_type" placeholder="请选择任务类型" style="width: 100%"> <el-select
v-model="singleForm.task_type"
placeholder="请选择任务类型"
style="width: 100%"
>
<el-option label="实名认证轮询" value="polling:realname" /> <el-option label="实名认证轮询" value="polling:realname" />
<el-option label="卡数据轮询" value="polling:carddata" /> <el-option label="卡数据轮询" value="polling:carddata" />
<el-option label="套餐轮询" value="polling:package" /> <el-option label="套餐轮询" value="polling:package" />
@@ -278,277 +286,62 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue' import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules' import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules'
import type { import type {
ManualTriggerLog, ManualTriggerLog,
ManualTriggerHistoryQueryParams, ManualTriggerHistoryQueryParams,
BatchManualTriggerRequest, BatchManualTriggerRequest,
ConditionManualTriggerRequest, ConditionManualTriggerRequest,
SingleManualTriggerRequest, SingleManualTriggerRequest,
TaskType TaskType
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu' import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
const loading = ref(false) const loading = ref(false)
const tableData = ref<ManualTriggerLog[]>([]) const tableData = ref<ManualTriggerLog[]>([])
const tableRef = ref() const tableRef = ref()
const contextMenuRef = ref() const contextMenuRef = ref()
const currentClickRow = ref<ManualTriggerLog | null>(null) const currentClickRow = ref<ManualTriggerLog | null>(null)
const batchDialogVisible = ref(false) const batchDialogVisible = ref(false)
const conditionDialogVisible = ref(false) const conditionDialogVisible = ref(false)
const singleDialogVisible = ref(false) const singleDialogVisible = ref(false)
const batchFormRef = ref<FormInstance>() const batchFormRef = ref<FormInstance>()
const conditionFormRef = ref<FormInstance>() const conditionFormRef = ref<FormInstance>()
const singleFormRef = ref<FormInstance>() const singleFormRef = ref<FormInstance>()
const carrierList = ref<any[]>([]) const carrierList = ref<any[]>([])
const shopTreeData = ref<any[]>([]) const shopTreeData = ref<any[]>([])
const cardList = ref<any[]>([]) const cardList = ref<any[]>([])
// 分页数据 // 分页数据
const pagination = reactive({ const pagination = reactive({
page: 1, page: 1,
page_size: 10, page_size: 10,
total: 0 total: 0
})
// 搜索表单
const searchForm = reactive({
task_type: undefined as TaskType | undefined
})
// 批量触发表单
const batchForm = reactive({
task_type: '' as TaskType | '',
card_ids_text: ''
})
// 条件触发表单
const conditionForm = reactive<ConditionManualTriggerRequest>({
task_type: '' as TaskType | '',
limit: 100,
card_status: undefined,
card_type: undefined,
carrier_code: undefined,
enable_polling: undefined,
package_ids: undefined,
shop_id: undefined
})
// 单卡触发表单
const singleForm = reactive({
card_id: undefined as number | undefined,
task_type: '' as TaskType | ''
})
const batchRules: FormRules = {
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
}
const conditionRules: FormRules = {
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
limit: [{ required: true, message: '请输入限制数量', trigger: 'blur' }]
}
const singleRules: FormRules = {
card_id: [{ required: true, message: '请输入卡ID', trigger: 'blur' }],
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
}
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
label: '任务类型',
prop: 'task_type',
type: 'select',
config: {
clearable: true,
placeholder: '请选择任务类型'
},
options: () => [
{ label: '实名认证轮询', value: 'polling:realname' },
{ label: '卡数据轮询', value: 'polling:carddata' },
{ label: '套餐轮询', value: 'polling:package' }
]
}
])
const getStatusTag = (status: string) => {
const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
pending: 'info',
processing: 'warning',
completed: 'success',
cancelled: 'danger'
}
return typeMap[status] || 'info'
}
const getProgressPercentage = (row: ManualTriggerLog) => {
if (row.total_count === 0) return 0
return Math.round((row.processed_count / row.total_count) * 100)
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'task_type_name',
label: '任务类型',
minWidth: 150
},
{
prop: 'trigger_type_name',
label: '触发类型',
minWidth: 120
},
{
prop: 'status',
label: '状态',
minWidth: 100,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: getStatusTag(row.status) }, () => row.status_name)
}
},
{
label: '进度',
minWidth: 200,
slots: {
default: ({ row }: any) =>
h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
h('span', { style: 'font-size: 12px;' }, `${row.processed_count}/${row.total_count}`),
h('el-progress', {
percentage: getProgressPercentage(row),
'stroke-width': 6,
'show-text': false,
style: 'flex: 1;'
})
])
}
},
{
prop: 'success_count',
label: '成功数',
minWidth: 100
},
{
prop: 'failed_count',
label: '失败数',
minWidth: 100
},
{
prop: 'triggered_at',
label: '触发时间',
minWidth: 180,
formatter: (row: any) => formatDateTime(row.triggered_at)
},
{
prop: 'completed_at',
label: '完成时间',
minWidth: 180,
formatter: (row: any) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
const contextMenuItems = computed<MenuItemType[]>(() => {
const items: MenuItemType[] = [{ label: '刷新', key: 'refresh' }]
if (
currentClickRow.value &&
(currentClickRow.value.status === 'pending' || currentClickRow.value.status === 'processing')
) {
items.push({
label: '取消任务',
key: 'cancel',
permission: 'polling_manual_trigger_cancel'
})
}
return items
})
const loadData = async () => {
loading.value = true
try {
const queryParams: ManualTriggerHistoryQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
...searchForm
}
const { data } = await ManualTriggerService.getHistory(queryParams)
tableData.value = data.items
pagination.total = data.total
} catch (error) {
ElMessage.error('加载触发历史失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
loadData()
}
const handleReset = () => {
Object.assign(searchForm, {
task_type: undefined
}) })
pagination.page = 1
loadData()
}
const handleRefresh = () => { // 搜索表单
loadData() const searchForm = reactive({
} task_type: undefined as TaskType | undefined
})
const handleSizeChange = (size: number) => { // 批量触发表单
pagination.page_size = size const batchForm = reactive({
loadData() task_type: '' as TaskType | '',
}
const handleCurrentChange = (page: number) => {
pagination.page = page
loadData()
}
const showBatchTriggerDialog = () => {
Object.assign(batchForm, {
task_type: '',
card_ids_text: '' card_ids_text: ''
}) })
batchDialogVisible.value = true
nextTick(() => {
batchFormRef.value?.clearValidate()
})
}
const showConditionTriggerDialog = () => { // 条件触发表单
Object.assign(conditionForm, { const conditionForm = reactive<ConditionManualTriggerRequest>({
task_type: '', task_type: '' as TaskType | '',
limit: 100, limit: 100,
card_status: undefined, card_status: undefined,
card_type: undefined, card_type: undefined,
@@ -557,207 +350,422 @@ const showConditionTriggerDialog = () => {
package_ids: undefined, package_ids: undefined,
shop_id: undefined shop_id: undefined
}) })
conditionDialogVisible.value = true
nextTick(() => { // 单卡触发表单
conditionFormRef.value?.clearValidate() const singleForm = reactive({
card_id: undefined as number | undefined,
task_type: '' as TaskType | ''
}) })
}
const showSingleTriggerDialog = () => { const batchRules: FormRules = {
Object.assign(singleForm, { task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
card_id: undefined, }
task_type: ''
})
singleDialogVisible.value = true
loadCards() // 加载初始卡列表
nextTick(() => {
singleFormRef.value?.clearValidate()
})
}
const handleBatchSubmit = async () => { const conditionRules: FormRules = {
if (!batchFormRef.value) return task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
await batchFormRef.value.validate(async (valid) => { limit: [{ required: true, message: '请输入限制数量', trigger: 'blur' }]
if (!valid) return }
try { const singleRules: FormRules = {
// 解析卡ID列表 card_id: [{ required: true, message: '请输入卡ID', trigger: 'blur' }],
const cardIds = batchForm.card_ids_text task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
.split(/[,\n]/) }
.map((id) => id.trim())
.filter((id) => id)
.map((id) => parseInt(id))
.filter((id) => !isNaN(id))
if (cardIds.length > 1000) { const {
ElMessage.warning('卡ID数量不能超过1000个') showContextMenuHint,
return hintPosition,
} getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const requestData: BatchManualTriggerRequest = { // 搜索表单配置
task_type: batchForm.task_type as TaskType, const searchFormItems = computed<SearchFormItem[]>(() => [
card_ids: cardIds.length > 0 ? cardIds : undefined {
} label: '任务类型',
prop: 'task_type',
await ManualTriggerService.batchTrigger(requestData) type: 'select',
ElMessage.success('批量触发成功') config: {
batchDialogVisible.value = false clearable: true,
loadData() placeholder: '请选择任务类型'
} catch (error) { },
ElMessage.error('批量触发失败') options: () => [
{ label: '实名认证轮询', value: 'polling:realname' },
{ label: '卡数据轮询', value: 'polling:carddata' },
{ label: '套餐轮询', value: 'polling:package' }
]
} }
}) ])
}
const handleConditionSubmit = async () => { const getStatusTag = (status: string) => {
if (!conditionFormRef.value) return const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
await conditionFormRef.value.validate(async (valid) => { pending: 'info',
if (!valid) return processing: 'warning',
completed: 'success',
try { cancelled: 'danger'
const requestData: ConditionManualTriggerRequest = {
task_type: conditionForm.task_type as TaskType,
limit: conditionForm.limit,
card_status: conditionForm.card_status,
card_type: conditionForm.card_type,
carrier_code: conditionForm.carrier_code,
enable_polling: conditionForm.enable_polling,
package_ids: conditionForm.package_ids,
shop_id: conditionForm.shop_id
}
await ManualTriggerService.conditionTrigger(requestData)
ElMessage.success('条件触发成功')
conditionDialogVisible.value = false
loadData()
} catch (error) {
ElMessage.error('条件触发失败')
} }
}) return typeMap[status] || 'info'
} }
const handleSingleSubmit = async () => { const getProgressPercentage = (row: ManualTriggerLog) => {
if (!singleFormRef.value) return if (row.total_count === 0) return 0
await singleFormRef.value.validate(async (valid) => { return Math.round((row.processed_count / row.total_count) * 100)
if (!valid) return }
try { // 动态列配置
await ManualTriggerService.singleTrigger({ const { columnChecks, columns } = useCheckedColumns(() => [
card_id: singleForm.card_id!, {
task_type: singleForm.task_type as TaskType prop: 'task_type_name',
label: '任务类型',
minWidth: 150
},
{
prop: 'trigger_type_name',
label: '触发类型',
minWidth: 120
},
{
prop: 'status',
label: '状态',
minWidth: 100,
slots: {
default: ({ row }: any) =>
h(ElTag, { type: getStatusTag(row.status) }, () => row.status_name)
}
},
{
label: '进度',
minWidth: 200,
slots: {
default: ({ row }: any) =>
h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
h('span', { style: 'font-size: 12px;' }, `${row.processed_count}/${row.total_count}`),
h('el-progress', {
percentage: getProgressPercentage(row),
'stroke-width': 6,
'show-text': false,
style: 'flex: 1;'
})
])
}
},
{
prop: 'success_count',
label: '成功数',
minWidth: 100
},
{
prop: 'failed_count',
label: '失败数',
minWidth: 100
},
{
prop: 'triggered_at',
label: '触发时间',
minWidth: 180,
formatter: (row: any) => formatDateTime(row.triggered_at)
},
{
prop: 'completed_at',
label: '完成时间',
minWidth: 180,
formatter: (row: any) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
}
])
const columnOptions = computed(() =>
columns.value.map((col) => ({
label: col.label,
prop: col.prop || col.label,
visible: true
}))
)
const contextMenuItems = computed<MenuItemType[]>(() => {
const items: MenuItemType[] = [{ label: '刷新', key: 'refresh' }]
if (
currentClickRow.value &&
(currentClickRow.value.status === 'pending' || currentClickRow.value.status === 'processing')
) {
items.push({
label: '取消任务',
key: 'cancel',
permission: 'polling_manual_trigger_cancel'
}) })
ElMessage.success('单卡触发成功') }
singleDialogVisible.value = false
return items
})
const loadData = async () => {
loading.value = true
try {
const queryParams: ManualTriggerHistoryQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
...searchForm
}
const { data } = await ManualTriggerService.getHistory(queryParams)
tableData.value = data.items
pagination.total = data.total
} catch (error) {
ElMessage.error('加载触发历史失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
loadData()
}
const handleReset = () => {
Object.assign(searchForm, {
task_type: undefined
})
pagination.page = 1
loadData()
}
const handleRefresh = () => {
loadData()
}
const handleSizeChange = (size: number) => {
pagination.page_size = size
loadData()
}
const handleCurrentChange = (page: number) => {
pagination.page = page
loadData()
}
const showBatchTriggerDialog = () => {
Object.assign(batchForm, {
task_type: '',
card_ids_text: ''
})
batchDialogVisible.value = true
nextTick(() => {
batchFormRef.value?.clearValidate()
})
}
const showConditionTriggerDialog = () => {
Object.assign(conditionForm, {
task_type: '',
limit: 100,
card_status: undefined,
card_type: undefined,
carrier_code: undefined,
enable_polling: undefined,
package_ids: undefined,
shop_id: undefined
})
conditionDialogVisible.value = true
nextTick(() => {
conditionFormRef.value?.clearValidate()
})
}
const showSingleTriggerDialog = () => {
Object.assign(singleForm, {
card_id: undefined,
task_type: ''
})
singleDialogVisible.value = true
loadCards() // 加载初始卡列表
nextTick(() => {
singleFormRef.value?.clearValidate()
})
}
const handleBatchSubmit = async () => {
if (!batchFormRef.value) return
await batchFormRef.value.validate(async (valid) => {
if (!valid) return
try {
// 解析卡ID列表
const cardIds = batchForm.card_ids_text
.split(/[,\n]/)
.map((id) => id.trim())
.filter((id) => id)
.map((id) => parseInt(id))
.filter((id) => !isNaN(id))
if (cardIds.length > 1000) {
ElMessage.warning('卡ID数量不能超过1000个')
return
}
const requestData: BatchManualTriggerRequest = {
task_type: batchForm.task_type as TaskType,
card_ids: cardIds.length > 0 ? cardIds : undefined
}
await ManualTriggerService.batchTrigger(requestData)
ElMessage.success('批量触发成功')
batchDialogVisible.value = false
loadData()
} catch (error) {
ElMessage.error('批量触发失败')
}
})
}
const handleConditionSubmit = async () => {
if (!conditionFormRef.value) return
await conditionFormRef.value.validate(async (valid) => {
if (!valid) return
try {
const requestData: ConditionManualTriggerRequest = {
task_type: conditionForm.task_type as TaskType,
limit: conditionForm.limit,
card_status: conditionForm.card_status,
card_type: conditionForm.card_type,
carrier_code: conditionForm.carrier_code,
enable_polling: conditionForm.enable_polling,
package_ids: conditionForm.package_ids,
shop_id: conditionForm.shop_id
}
await ManualTriggerService.conditionTrigger(requestData)
ElMessage.success('条件触发成功')
conditionDialogVisible.value = false
loadData()
} catch (error) {
ElMessage.error('条件触发失败')
}
})
}
const handleSingleSubmit = async () => {
if (!singleFormRef.value) return
await singleFormRef.value.validate(async (valid) => {
if (!valid) return
try {
await ManualTriggerService.singleTrigger({
card_id: singleForm.card_id!,
task_type: singleForm.task_type as TaskType
})
ElMessage.success('单卡触发成功')
singleDialogVisible.value = false
loadData()
} catch (error) {
ElMessage.error('单卡触发失败')
}
})
}
const handleCancelTask = async (row: ManualTriggerLog) => {
try {
await ElMessageBox.confirm('确定要取消该任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await ManualTriggerService.cancelTrigger({ trigger_id: row.id })
ElMessage.success('任务已取消')
loadData() loadData()
} catch (error) { } catch (error) {
ElMessage.error('单卡触发失败') if (error !== 'cancel') {
ElMessage.error('取消任务失败')
}
} }
}) }
}
const handleCancelTask = async (row: ManualTriggerLog) => { const handleRowContextMenu = (row: ManualTriggerLog, column: any, event: MouseEvent) => {
try { event.preventDefault()
await ElMessageBox.confirm('确定要取消该任务吗?', '提示', { event.stopPropagation()
confirmButtonText: '确定', currentClickRow.value = row
cancelButtonText: '取消', contextMenuRef.value?.show(event)
type: 'warning' }
})
await ManualTriggerService.cancelTrigger({ trigger_id: row.id }) const handleContextMenuSelect = (item: MenuItemType) => {
ElMessage.success('任务已取消') if (!currentClickRow.value) return
switch (item.key) {
case 'refresh':
loadData()
break
case 'cancel':
handleCancelTask(currentClickRow.value)
break
}
}
// 加载运营商列表
const loadCarriers = async (carrierName?: string) => {
try {
const { data } = await CarrierService.getCarriers({
page: 1,
page_size: 20,
carrier_name: carrierName
})
carrierList.value = data.items || data.list || []
} catch (error) {
console.error('加载运营商列表失败', error)
}
}
const remoteSearchCarrier = (query: string) => {
if (query) {
loadCarriers(query)
} else {
loadCarriers()
}
}
// 加载店铺树形数据
const loadShops = async () => {
try {
const { data } = await ShopService.getShops()
shopTreeData.value = data.items || data.list || []
} catch (error) {
console.error('加载店铺列表失败', error)
}
}
// 加载卡列表
const loadCards = async (iccid?: string) => {
try {
const { data } = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20,
iccid: iccid
})
cardList.value = data.items || data.list || []
} catch (error) {
console.error('加载卡列表失败', error)
}
}
const remoteSearchCard = (query: string) => {
if (query) {
loadCards(query)
} else {
loadCards()
}
}
onMounted(() => {
loadData() loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('取消任务失败')
}
}
}
const handleRowContextMenu = (row: ManualTriggerLog, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentClickRow.value = row
contextMenuRef.value?.show(event)
}
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentClickRow.value) return
switch (item.key) {
case 'refresh':
loadData()
break
case 'cancel':
handleCancelTask(currentClickRow.value)
break
}
}
// 加载运营商列表
const loadCarriers = async (carrierName?: string) => {
try {
const { data } = await CarrierService.getCarriers({
page: 1,
page_size: 20,
carrier_name: carrierName
})
carrierList.value = data.items || data.list || []
} catch (error) {
console.error('加载运营商列表失败', error)
}
}
const remoteSearchCarrier = (query: string) => {
if (query) {
loadCarriers(query)
} else {
loadCarriers() loadCarriers()
} loadShops()
} })
// 加载店铺树形数据
const loadShops = async () => {
try {
const { data } = await ShopService.getShops()
shopTreeData.value = data.items || data.list || []
} catch (error) {
console.error('加载店铺列表失败', error)
}
}
// 加载卡列表
const loadCards = async (iccid?: string) => {
try {
const { data } = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20,
iccid: iccid
})
cardList.value = data.items || data.list || []
} catch (error) {
console.error('加载卡列表失败', error)
}
}
const remoteSearchCard = (query: string) => {
if (query) {
loadCards(query)
} else {
loadCards()
}
}
onMounted(() => {
loadData()
loadCarriers()
loadShops()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.manual-trigger-page { .manual-trigger-page {
height: 100%; display: flex;
display: flex; flex-direction: column;
flex-direction: column; height: 100%;
} }
</style> </style>

View File

@@ -290,17 +290,17 @@
.stat-card { .stat-card {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
.stat-icon { .stat-icon {
width: 64px;
height: 64px;
border-radius: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #ffffff; width: 64px;
height: 64px;
color: #fff;
border-radius: 12px;
&.total { &.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@@ -327,9 +327,9 @@
flex: 1; flex: 1;
.stat-label { .stat-label {
margin-bottom: 4px;
font-size: 14px; font-size: 14px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
margin-bottom: 4px;
} }
.stat-value { .stat-value {
@@ -345,8 +345,8 @@
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
font-weight: bold; font-weight: bold;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }
@@ -361,14 +361,14 @@
.info-item { .info-item {
.label { .label {
color: var(--el-text-color-secondary);
font-size: 14px; font-size: 14px;
color: var(--el-text-color-secondary);
} }
.value { .value {
color: var(--el-text-color-primary);
font-weight: 500;
margin-left: 8px; margin-left: 8px;
font-weight: 500;
color: var(--el-text-color-primary);
} }
} }
} }

View File

@@ -189,7 +189,7 @@
:value="role.ID" :value="role.ID"
> >
<div <div
style="display: flex; justify-content: space-between; align-items: center" style="display: flex; align-items: center; justify-content: space-between"
> >
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag type="success" size="small">客户角色</ElTag> <ElTag type="success" size="small">客户角色</ElTag>
@@ -273,7 +273,7 @@
</div> </div>
</ElOption> </ElOption>
</ElSelect> </ElSelect>
<div style="margin-top: 8px; color: #909399; font-size: 12px"> <div style="margin-top: 8px; font-size: 12px; color: #909399">
只能选择一个客户角色选择后立即生效 只能选择一个客户角色选择后立即生效
</div> </div>
</div> </div>
@@ -1076,10 +1076,10 @@
} }
.current-role-display { .current-role-display {
border-radius: 8px;
padding: 16px 20px; padding: 16px 20px;
margin-bottom: 24px; margin-bottom: 24px;
border: 2px solid var(--el-color-primary); border: 2px solid var(--el-color-primary);
border-radius: 8px;
&.no-role { &.no-role {
border: 2px dashed var(--el-border-color); border: 2px dashed var(--el-border-color);
@@ -1090,30 +1090,30 @@
} }
.current-role-label { .current-role-label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-bottom: 12px; margin-bottom: 12px;
font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--el-text-color-secondary);
} }
.current-role-value { .current-role-value {
display: flex;
align-items: center;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: var(--el-color-primary); color: var(--el-color-primary);
display: flex;
align-items: center;
} }
} }
.default-roles-section { .default-roles-section {
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
margin-bottom: 4px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #303133; color: #303133;
margin-bottom: 4px;
} }
} }

View File

@@ -279,8 +279,8 @@
.detail-header { .detail-header {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
padding-bottom: 16px; padding-bottom: 16px;
.detail-title { .detail-title {
@@ -294,10 +294,10 @@
.loading-container { .loading-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
.el-icon { .el-icon {

View File

@@ -64,7 +64,13 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="handleDialogClosed" @closed="handleDialogClosed"
> >
<ElForm ref="formRef" :model="form" :rules="rules" label-width="140px" class="config-form"> <ElForm
ref="formRef"
:model="form"
:rules="rules"
label-width="140px"
class="config-form"
>
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="配置名称" prop="name"> <ElFormItem label="配置名称" prop="name">
@@ -103,10 +109,7 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="小程序AppID" prop="miniapp_app_id"> <ElFormItem label="小程序AppID" prop="miniapp_app_id">
<ElInput <ElInput v-model="form.miniapp_app_id" placeholder="请输入小程序AppID" />
v-model="form.miniapp_app_id"
placeholder="请输入小程序AppID"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
@@ -127,10 +130,7 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="公众号AppID" prop="oa_app_id"> <ElFormItem label="公众号AppID" prop="oa_app_id">
<ElInput <ElInput v-model="form.oa_app_id" placeholder="请输入公众号AppID" />
v-model="form.oa_app_id"
placeholder="请输入公众号AppID"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
@@ -167,10 +167,7 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url"> <ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
<ElInput <ElInput v-model="form.oa_oauth_redirect_url" placeholder="请输入OAuth回调地址" />
v-model="form.oa_oauth_redirect_url"
placeholder="请输入OAuth回调地址"
/>
</ElFormItem> </ElFormItem>
<ElDivider content-position="left"> <ElDivider content-position="left">
@@ -179,18 +176,12 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="微信商户号" prop="wx_mch_id"> <ElFormItem label="微信商户号" prop="wx_mch_id">
<ElInput <ElInput v-model="form.wx_mch_id" placeholder="请输入微信商户号" />
v-model="form.wx_mch_id"
placeholder="请输入微信商户号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="证书序列号" prop="wx_serial_no"> <ElFormItem label="证书序列号" prop="wx_serial_no">
<ElInput <ElInput v-model="form.wx_serial_no" placeholder="请输入微信证书序列号" />
v-model="form.wx_serial_no"
placeholder="请输入微信证书序列号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -217,10 +208,7 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElFormItem label="支付回调地址" prop="wx_notify_url"> <ElFormItem label="支付回调地址" prop="wx_notify_url">
<ElInput <ElInput v-model="form.wx_notify_url" placeholder="请输入微信支付回调地址" />
v-model="form.wx_notify_url"
placeholder="请输入微信支付回调地址"
/>
</ElFormItem> </ElFormItem>
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
@@ -252,10 +240,7 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="小程序AppID" prop="miniapp_app_id"> <ElFormItem label="小程序AppID" prop="miniapp_app_id">
<ElInput <ElInput v-model="form.miniapp_app_id" placeholder="请输入小程序AppID" />
v-model="form.miniapp_app_id"
placeholder="请输入小程序AppID"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
@@ -276,10 +261,7 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="公众号AppID" prop="oa_app_id"> <ElFormItem label="公众号AppID" prop="oa_app_id">
<ElInput <ElInput v-model="form.oa_app_id" placeholder="请输入公众号AppID" />
v-model="form.oa_app_id"
placeholder="请输入公众号AppID"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
@@ -316,10 +298,7 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url"> <ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
<ElInput <ElInput v-model="form.oa_oauth_redirect_url" placeholder="请输入OAuth回调地址" />
v-model="form.oa_oauth_redirect_url"
placeholder="请输入OAuth回调地址"
/>
</ElFormItem> </ElFormItem>
<ElDivider content-position="left"> <ElDivider content-position="left">
@@ -328,18 +307,12 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="微信商户号" prop="wx_mch_id"> <ElFormItem label="微信商户号" prop="wx_mch_id">
<ElInput <ElInput v-model="form.wx_mch_id" placeholder="请输入微信商户号" />
v-model="form.wx_mch_id"
placeholder="请输入微信商户号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="证书序列号" prop="wx_serial_no"> <ElFormItem label="证书序列号" prop="wx_serial_no">
<ElInput <ElInput v-model="form.wx_serial_no" placeholder="请输入微信证书序列号" />
v-model="form.wx_serial_no"
placeholder="请输入微信证书序列号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -366,10 +339,7 @@
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElFormItem label="支付回调地址" prop="wx_notify_url"> <ElFormItem label="支付回调地址" prop="wx_notify_url">
<ElInput <ElInput v-model="form.wx_notify_url" placeholder="请输入微信支付回调地址" />
v-model="form.wx_notify_url"
placeholder="请输入微信支付回调地址"
/>
</ElFormItem> </ElFormItem>
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
@@ -400,44 +370,29 @@
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="富友API地址" prop="fy_api_url"> <ElFormItem label="富友API地址" prop="fy_api_url">
<ElInput <ElInput v-model="form.fy_api_url" placeholder="请输入富友API地址" />
v-model="form.fy_api_url"
placeholder="请输入富友API地址"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="富友机构号" prop="fy_ins_cd"> <ElFormItem label="富友机构号" prop="fy_ins_cd">
<ElInput <ElInput v-model="form.fy_ins_cd" placeholder="请输入富友机构号" />
v-model="form.fy_ins_cd"
placeholder="请输入富友机构号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="富友商户号" prop="fy_mchnt_cd"> <ElFormItem label="富友商户号" prop="fy_mchnt_cd">
<ElInput <ElInput v-model="form.fy_mchnt_cd" placeholder="请输入富友商户号" />
v-model="form.fy_mchnt_cd"
placeholder="请输入富友商户号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="12"> <ElCol :span="12">
<ElFormItem label="富友终端号" prop="fy_term_id"> <ElFormItem label="富友终端号" prop="fy_term_id">
<ElInput <ElInput v-model="form.fy_term_id" placeholder="请输入富友终端号" />
v-model="form.fy_term_id"
placeholder="请输入富友终端号"
/>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
</ElRow> </ElRow>
<ElFormItem label="支付回调地址" prop="fy_notify_url"> <ElFormItem label="支付回调地址" prop="fy_notify_url">
<ElInput <ElInput v-model="form.fy_notify_url" placeholder="请输入富友支付回调地址" />
v-model="form.fy_notify_url"
placeholder="请输入富友支付回调地址"
/>
</ElFormItem> </ElFormItem>
<ElRow :gutter="20"> <ElRow :gutter="20">
<ElCol :span="12"> <ElCol :span="12">
@@ -1117,8 +1072,8 @@
.config-form { .config-form {
max-height: 600px; max-height: 600px;
overflow-y: auto;
padding-right: 10px; padding-right: 10px;
overflow-y: auto;
.divider-title { .divider-title {
font-size: 14px; font-size: 14px;
@@ -1127,7 +1082,7 @@
} }
:deep(.el-divider) { :deep(.el-divider) {
margin: 24px 0 20px 0; margin: 24px 0 20px;
} }
:deep(.el-form-item) { :deep(.el-form-item) {

View File

@@ -143,7 +143,7 @@
class="permission-tree" class="permission-tree"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span style="display: flex; align-items: center; gap: 8px"> <span style="display: flex; gap: 8px; align-items: center">
<span>{{ node.label }}</span> <span>{{ node.label }}</span>
<ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small"> <ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small">
{{ data.perm_type === 1 ? '菜单' : '按钮' }} {{ data.perm_type === 1 ? '菜单' : '按钮' }}
@@ -519,7 +519,7 @@
} }
// 检查所有顶层节点 // 检查所有顶层节点
availablePermissions.value.forEach(node => checkNode(node)) availablePermissions.value.forEach((node) => checkNode(node))
return Array.from(parentIdsToHalfCheck) return Array.from(parentIdsToHalfCheck)
} }
@@ -568,7 +568,8 @@
label: node.perm_name, label: node.perm_name,
perm_type: node.perm_type, perm_type: node.perm_type,
status: node.status ?? 1, status: node.status ?? 1,
children: node.children && node.children.length > 0 ? buildTreeData(node.children) : undefined children:
node.children && node.children.length > 0 ? buildTreeData(node.children) : undefined
})) }))
} }
@@ -692,7 +693,11 @@
const parentIds: number[] = [] const parentIds: number[] = []
// 在原始权限树中递归查找父节点 // 在原始权限树中递归查找父节点
const findInTree = (treeNodes: PermissionTreeNode[], targetId: number, parentId?: number): number | null => { const findInTree = (
treeNodes: PermissionTreeNode[],
targetId: number,
parentId?: number
): number | null => {
for (const node of treeNodes) { for (const node of treeNodes) {
if (node.id === targetId) { if (node.id === targetId) {
return parentId || null return parentId || null
@@ -727,7 +732,7 @@
// 为每个勾选的节点找到所有父节点 // 为每个勾选的节点找到所有父节点
checkedKeys.forEach((key: number) => { checkedKeys.forEach((key: number) => {
const parents = getParentNodeIds(key) const parents = getParentNodeIds(key)
parents.forEach(parentId => allIds.add(parentId)) parents.forEach((parentId) => allIds.add(parentId))
}) })
return Array.from(allIds) return Array.from(allIds)
@@ -1079,8 +1084,8 @@
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
.dialog-title { .dialog-title {
@@ -1090,8 +1095,8 @@
.role-info { .role-info {
display: flex; display: flex;
align-items: center;
gap: 8px; gap: 8px;
align-items: center;
.role-name { .role-name {
font-size: 14px; font-size: 14px;
@@ -1102,25 +1107,25 @@
.permission-tree-transfer-container { .permission-tree-transfer-container {
display: flex; display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px; gap: 20px;
padding: 20px 0; align-items: stretch;
justify-content: space-between;
min-height: 500px; min-height: 500px;
padding: 20px 0;
.transfer-panel { .transfer-panel {
flex: 1;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
max-width: 340px;
overflow: hidden;
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
border-radius: 4px; border-radius: 4px;
overflow: hidden;
max-width: 340px;
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
padding: 12px 16px; padding: 12px 16px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color); border-bottom: 1px solid var(--el-border-color);
@@ -1134,8 +1139,8 @@
.panel-body { .panel-body {
flex: 1; flex: 1;
overflow-y: auto;
padding: 12px; padding: 12px;
overflow-y: auto;
.permission-tree { .permission-tree {
:deep(.el-tree-node) { :deep(.el-tree-node) {
@@ -1144,8 +1149,8 @@
:deep(.el-tree-node__content) { :deep(.el-tree-node__content) {
height: 36px; height: 36px;
line-height: 36px;
padding-right: 8px; padding-right: 8px;
line-height: 36px;
} }
:deep(.el-tree-node__label) { :deep(.el-tree-node__label) {
@@ -1162,16 +1167,16 @@
.tree-node-content { .tree-node-content {
display: flex; display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px; gap: 8px;
align-items: center;
justify-content: space-between;
width: 100%;
.tree-node-label { .tree-node-label {
flex: 1;
display: flex; display: flex;
align-items: center; flex: 1;
gap: 8px; gap: 8px;
align-items: center;
} }
.el-button { .el-button {
@@ -1186,9 +1191,9 @@
.transfer-buttons { .transfer-buttons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
justify-content: center;
padding: 0 10px; padding: 0 10px;
.el-button { .el-button {