This commit is contained in:
@@ -35,20 +35,24 @@
|
||||
根据提供的权限JSON,需要添加权限控制的模块包括:
|
||||
|
||||
1. **套餐管理** (`/package-management`)
|
||||
|
||||
- 套餐系列 (`package-series`)
|
||||
- 套餐列表 (`package-list`)
|
||||
- 单套餐分配 (`package-assign`)
|
||||
- 套餐系列分配 (`series-assign`)
|
||||
|
||||
2. **店铺管理** (`/shop-management`)
|
||||
|
||||
- 店铺列表 (`list`)
|
||||
|
||||
3. **账号管理** (`/account-management`)
|
||||
|
||||
- 账号列表 (`account`)
|
||||
- 平台账号 (`platform-account`)
|
||||
- 代理账号 (`shop-account`)
|
||||
|
||||
4. **资产管理** (`/asset-management`)
|
||||
|
||||
- IoT卡管理 (`iot-card-management`)
|
||||
- IoT卡任务 (`iot-card-task`)
|
||||
- 设备任务 (`device-task`)
|
||||
@@ -67,9 +71,9 @@
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const { hasAuth } = useAuth()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -79,29 +83,28 @@ const { hasAuth } = useAuth()
|
||||
</template>
|
||||
|
||||
<!-- 2. 表格列中的状态切换 -->
|
||||
<ElSwitch
|
||||
:disabled="!hasAuth('module:update_status')"
|
||||
v-model="row.status"
|
||||
/>
|
||||
<ElSwitch :disabled="!hasAuth('module:update_status')" v-model="row.status" />
|
||||
|
||||
<!-- 3. 表格列中的操作按钮 -->
|
||||
<script>
|
||||
const columns = [{
|
||||
prop: 'operation',
|
||||
formatter: (row) => {
|
||||
const buttons = []
|
||||
const columns = [
|
||||
{
|
||||
prop: 'operation',
|
||||
formatter: (row) => {
|
||||
const buttons = []
|
||||
|
||||
if (hasAuth('module:edit')) {
|
||||
buttons.push(h(ArtButtonTable, { type: 'edit', onClick: () => edit(row) }))
|
||||
if (hasAuth('module:edit')) {
|
||||
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>
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
### 1. 套餐管理模块 (Package Management) ✅
|
||||
|
||||
- [x] **套餐系列页面** (`/package-management/package-series`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增套餐系列"按钮添加 `v-permission="'package_series:add'"`
|
||||
- ✅ 为状态切换添加 `disabled: !hasAuth('package_series:update_status')`
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断编辑权限 (`package_series:edit`)和删除权限 (`package_series:delete`)
|
||||
|
||||
- [x] **套餐列表页面** (`/package-management/package-list`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增套餐"按钮添加 `v-permission="'package:add'"`
|
||||
- ✅ 为上/下架切换添加 `disabled: !hasAuth('package:update_away')`
|
||||
@@ -22,6 +24,7 @@
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断编辑 (`package:edit`) 和删除 (`package:delete`) 权限
|
||||
|
||||
- [x] **单套餐分配页面** (`/package-management/package-assign`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增分配"按钮添加 `v-permission="'package_assign:add'"`
|
||||
- ✅ 为状态切换添加 `disabled: !hasAuth('package_assign:update_status')`
|
||||
@@ -50,6 +53,7 @@
|
||||
### 3. 账号管理模块 (Account Management) ✅
|
||||
|
||||
- [x] **账号列表页面** (`/account-management/account`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增账号"按钮添加 `v-permission="'account:add'"`
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||
@@ -58,6 +62,7 @@
|
||||
- 删除: `account:delete`
|
||||
|
||||
- [x] **平台账号页面** (`/account-management/platform-account`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增平台账号"按钮添加 `v-permission="'platform_account:add'"`
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||
@@ -76,6 +81,7 @@
|
||||
### 4. 资产管理模块 (Asset Management) ✅
|
||||
|
||||
- [x] **IoT卡管理页面** (`/asset-management/iot-card-management`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为表格头部操作按钮添加权限控制:
|
||||
- 批量分配: `v-permission="'iot_card:batch_allocation'"`
|
||||
@@ -89,16 +95,19 @@
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **IoT卡任务页面** (`/asset-management/iot-card-task`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"批量导入IoT卡"按钮添加 `v-permission="''"`
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **设备任务页面** (`/asset-management/device-task`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"批量导入设备"按钮添加 `v-permission="'device_task:bulk_import'"`
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **设备管理页面** (`/asset-management/device-list`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为表格头部操作按钮添加权限控制:
|
||||
- 批量分配: `v-permission="'devices:batch_allocation'"`
|
||||
@@ -117,6 +126,7 @@
|
||||
### 5. 账户管理模块 (Account Module) ✅
|
||||
|
||||
- [x] **企业客户页面** (`/account/enterprise-customer`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增企业客户"按钮添加 `v-permission="'enterprise_customer:add'"`
|
||||
- ✅ 在操作列中使用 `hasAuth()` 判断以下权限:
|
||||
@@ -127,6 +137,7 @@
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **运营商管理页面** (`/account/carrier-management`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增运营商"按钮添加 `v-permission="'carrier:add'"`
|
||||
- ✅ 为状态切换添加 `disabled: !hasAuth('carrier:update_status')`
|
||||
@@ -134,16 +145,19 @@
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **订单管理页面** (`/account/orders`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"创建订单"按钮添加 `v-permission="'orders:add'"`
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **提现配置页面** (`/account/commission/withdrawal-settings`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"新增配置"按钮添加 `v-permission="'withdrawal_settings:add'"`
|
||||
- ✅ 验证权限控制正确工作
|
||||
|
||||
- [x] **我的佣金页面** (`/account/commission/my-commission`)
|
||||
|
||||
- ✅ 引入 `useAuth` composable
|
||||
- ✅ 为"发起提现"按钮添加 `v-permission="'my_commission:add'"`
|
||||
- ✅ 验证权限控制正确工作
|
||||
@@ -164,6 +178,7 @@
|
||||
### 7. 测试与文档
|
||||
|
||||
- [ ] **权限控制测试**
|
||||
|
||||
- 创建不同权限组合的测试用户
|
||||
- 验证每个页面的按钮显示/隐藏符合预期
|
||||
- 验证状态切换的启用/禁用符合预期
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
## What Changes
|
||||
|
||||
### IoT Card Operations (6 new APIs)
|
||||
|
||||
- 查询流量使用情况 (GET /api/admin/iot-cards/{iccid}/gateway-flow)
|
||||
- 查询实名认证状态 (GET /api/admin/iot-cards/{iccid}/gateway-realname)
|
||||
- 查询卡片状态 (GET /api/admin/iot-cards/{iccid}/gateway-status)
|
||||
@@ -15,6 +16,7 @@
|
||||
- 停用物联网卡 (POST /api/admin/iot-cards/{iccid}/stop)
|
||||
|
||||
### Device Operations (6 new APIs)
|
||||
|
||||
- 重启设备 (POST /api/admin/devices/by-imei/{imei}/reboot)
|
||||
- 重置设备 (POST /api/admin/devices/by-imei/{imei}/reset)
|
||||
- 设置限速 (PUT /api/admin/devices/by-imei/{imei}/speed-limit)
|
||||
@@ -22,6 +24,7 @@
|
||||
- 设置WiFi (PUT /api/admin/devices/by-imei/{imei}/wifi)
|
||||
|
||||
### UI Changes
|
||||
|
||||
- 在 `/asset-management/iot-card-management` 页面添加操作按钮:
|
||||
- "查询流量"按钮(主要操作)
|
||||
- "更多操作"下拉菜单(包含其他5个操作)
|
||||
@@ -32,10 +35,12 @@
|
||||
## Impact
|
||||
|
||||
### Affected Specs
|
||||
|
||||
- **NEW**: `specs/iot-card-operations/spec.md` - IoT卡操作相关的所有需求
|
||||
- **NEW**: `specs/device-operations/spec.md` - 设备操作相关的所有需求
|
||||
|
||||
### Affected Code
|
||||
|
||||
- **API层**:
|
||||
- 新增 `src/api/modules/iotCard.ts` - IoT卡操作API方法
|
||||
- 新增 `src/api/modules/device.ts` - 设备操作API方法
|
||||
@@ -47,6 +52,7 @@
|
||||
- 修改 `src/views/asset-management/devices/index.vue` - 添加操作按钮和对话框
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
无破坏性变更。所有变更都是增量式的新功能添加。
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
**API端点**: `GET /api/admin/shops/{shop_id}/roles`
|
||||
|
||||
**路径参数**:
|
||||
|
||||
- `shop_id` (integer, required): 店铺ID
|
||||
|
||||
**响应数据**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
shop_id: number // 店铺ID
|
||||
@@ -54,9 +56,11 @@ interface ShopRole {
|
||||
**API端点**: `POST /api/admin/shops/{shop_id}/roles`
|
||||
|
||||
**路径参数**:
|
||||
|
||||
- `shop_id` (integer, required): 店铺ID
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
role_ids: number[] | null // 角色ID列表
|
||||
@@ -106,6 +110,7 @@ interface ShopRole {
|
||||
**API端点**: `DELETE /api/admin/shops/{shop_id}/roles/{role_id}`
|
||||
|
||||
**路径参数**:
|
||||
|
||||
- `shop_id` (integer, required): 店铺ID
|
||||
- `role_id` (integer, required): 角色ID
|
||||
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
### 完成的功能
|
||||
|
||||
1. **类型定义** - 已在 `src/types/api/shop.ts` 添加了三个接口:
|
||||
|
||||
- `ShopRoleResponse`: 店铺角色响应实体
|
||||
- `ShopRolesResponse`: 店铺角色列表响应
|
||||
- `AssignShopRolesRequest`: 分配角色请求
|
||||
|
||||
2. **API 服务层** - 已在 `src/api/modules/shop.ts` 的 `ShopService` 类中添加:
|
||||
|
||||
- `getShopRoles(shopId)`: 获取店铺默认角色列表
|
||||
- `assignShopRoles(shopId, data)`: 分配店铺默认角色
|
||||
- `deleteShopRole(shopId, roleId)`: 删除店铺默认角色
|
||||
@@ -56,6 +58,7 @@
|
||||
### 待测试项
|
||||
|
||||
所有开发任务已完成,现在需要进行功能测试以确保:
|
||||
|
||||
- API 调用正常工作
|
||||
- UI 交互符合预期
|
||||
- 边界情况处理正确
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: 重命名 4 个 API 端点的请求/响应字段
|
||||
|
||||
- `PATCH /api/admin/iot-cards/series-binding` - 请求参数 `series_allocation_id` → `series_id`
|
||||
- `PATCH /api/admin/devices/series-binding` - 请求参数 `series_allocation_id` → `series_id`
|
||||
- `GET /api/admin/iot-cards/standalone` - 查询参数和响应字段 `series_allocation_id` → `series_id`
|
||||
|
||||
@@ -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.
|
||||
|
||||
**Response Field**: `series_id` (number | null)
|
||||
|
||||
- Value is package series ID if device is bound to a series
|
||||
- Value is null if device has no series binding
|
||||
|
||||
@@ -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`
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"device_ids": [1, 2, 3],
|
||||
"series_id": 123 // Changed from series_allocation_id
|
||||
"series_id": 123 // Changed from series_allocation_id
|
||||
}
|
||||
```
|
||||
|
||||
**Field Definitions**:
|
||||
|
||||
- `device_ids`: Array of device IDs (max 500 items)
|
||||
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
|
||||
@@ -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.
|
||||
|
||||
**Response Field**: `series_id` (number | null)
|
||||
|
||||
- Value is package series ID if card is bound to a series
|
||||
- Value is null if card has no series binding
|
||||
|
||||
@@ -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`
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"iccids": ["898600..."],
|
||||
"series_id": 123 // Changed from series_allocation_id
|
||||
"series_id": 123 // Changed from series_allocation_id
|
||||
}
|
||||
```
|
||||
|
||||
**Field Definitions**:
|
||||
|
||||
- `iccids`: Array of ICCIDs (max 500 items)
|
||||
- `series_id`: Package series ID (tb_package_series.id), 0 means clear association
|
||||
|
||||
**Response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
## 1. 类型定义更新
|
||||
|
||||
### 1.1 批量设置接口参数重命名
|
||||
|
||||
- [x] 1.1.1 更新 `src/types/api/device.ts` 中 BatchSetDeviceSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
|
||||
- [x] 1.1.2 更新 `src/types/api/card.ts` 中 BatchSetCardSeriesBindingRequest 接口,将 `series_allocation_id` 改为 `series_id`
|
||||
|
||||
### 1.2 查询参数新增字段
|
||||
|
||||
- [x] 1.2.1 在 `src/types/api/device.ts` 的 DeviceQueryParams 接口中添加 `series_id?: number` 查询参数
|
||||
- [x] 1.2.2 在 `src/types/api/card.ts` 的 StandaloneCardQueryParams 接口中添加 `series_id?: number` 查询参数
|
||||
|
||||
### 1.3 响应类型新增字段
|
||||
|
||||
- [x] 1.3.1 在 `src/types/api/device.ts` 的 Device 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
|
||||
- [x] 1.3.2 在 `src/types/api/card.ts` 的 StandaloneIotCard 接口中添加 `series_id?: number | null` 响应字段(在 updated_at 后面)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
**修改**: `src/types/api/packageManagement.ts`
|
||||
|
||||
**套餐系列相关类型**:
|
||||
|
||||
- `PackageSeriesResponse` - 新增 `enable_one_time_commission` 和 `one_time_commission_config`
|
||||
- `CreatePackageSeriesRequest` - 新增一次性佣金配置字段
|
||||
- `UpdatePackageSeriesRequest` - 新增一次性佣金配置字段
|
||||
@@ -24,6 +25,7 @@
|
||||
- 新增 `OneTimeCommissionTier` - 梯度佣金档位配置
|
||||
|
||||
**系列分配相关类型**:
|
||||
|
||||
- **BREAKING**: `ShopSeriesAllocationResponse` - 完全重构字段结构
|
||||
- 移除: `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`
|
||||
@@ -32,6 +34,7 @@
|
||||
- 移除不再使用的类型: `BaseCommissionConfig`, `TierEntry`, `TierCommissionConfig`, `OneTimeCommissionTierEntry`, `OneTimeCommissionConfig`(旧版)
|
||||
|
||||
**单套餐分配相关类型**:
|
||||
|
||||
- `ShopPackageAllocationResponse` - 字段调整
|
||||
- 新增: `series_id`, `series_name`, `series_allocation_id`, `allocator_shop_id`, `allocator_shop_name`
|
||||
- 移除: `allocation_id` (重命名为 `series_allocation_id`), `calculated_cost_price`
|
||||
@@ -44,6 +47,7 @@
|
||||
### 3. 页面/组件适配(实施阶段处理)
|
||||
|
||||
以下文件需要适配新的数据结构(本提案阶段不编写代码):
|
||||
|
||||
- 套餐系列管理页面 - 需支持一次性佣金配置表单
|
||||
- 系列分配页面 - 需重构佣金配置表单UI
|
||||
- 单套餐分配页面 - 需展示新增的关联字段
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
套餐系列的数据模型SHALL支持一次性佣金配置,包括固定佣金、梯度佣金、强制充值、时效类型等高级功能。
|
||||
|
||||
**字段定义**:
|
||||
|
||||
- `id` (number) - 系列ID
|
||||
- `series_code` (string) - 系列编码,唯一标识
|
||||
- `series_name` (string) - 系列名称
|
||||
@@ -18,6 +19,7 @@
|
||||
- `updated_at` (string) - 更新时间
|
||||
|
||||
**一次性佣金配置结构** (`SeriesOneTimeCommissionConfig`):
|
||||
|
||||
- `enable` (boolean) - 是否启用一次性佣金
|
||||
- `commission_type` (string) - 佣金类型: "fixed"(固定) | "tiered"(梯度)
|
||||
- `commission_amount` (number, optional) - 固定佣金金额(分),commission_type=fixed时使用
|
||||
@@ -31,6 +33,7 @@
|
||||
- `validity_value` (string, optional) - 时效值(日期字符串或月数)
|
||||
|
||||
**梯度档位结构** (`OneTimeCommissionTier`):
|
||||
|
||||
- `threshold` (number) - 达标阈值
|
||||
- `dimension` (string) - 统计维度: "sales_count"(销量) | "sales_amount"(销售额)
|
||||
- `amount` (number) - 佣金金额(分)
|
||||
@@ -64,10 +67,7 @@
|
||||
- `commission_type`: "tiered"
|
||||
- `threshold`: 5000
|
||||
- `trigger_type`: "accumulated_recharge"
|
||||
- `tiers`: [
|
||||
{ threshold: 10, dimension: "sales_count", amount: 3000 },
|
||||
{ threshold: 50, dimension: "sales_count", amount: 8000 }
|
||||
]
|
||||
- `tiers`: [ { threshold: 10, dimension: "sales_count", amount: 3000 }, { threshold: 50, dimension: "sales_count", amount: 8000 } ]
|
||||
- `validity_type`: "relative"
|
||||
- `validity_value`: "12" (12个月)
|
||||
- **THEN** 系统SHALL创建套餐系列,并保存梯度佣金配置
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
单套餐分配的数据模型SHALL新增系列关联字段和分配者信息字段,以支持更完整的分配关系追溯。
|
||||
|
||||
**字段定义**:
|
||||
|
||||
- `id` (number) - 分配ID
|
||||
- `package_id` (number) - 套餐ID
|
||||
- `package_code` (string) - 套餐编码
|
||||
@@ -24,6 +25,7 @@
|
||||
- `updated_at` (string) - 更新时间
|
||||
|
||||
**查询参数** (`ShopPackageAllocationQueryParams`):
|
||||
|
||||
- `page` (number, optional) - 页码
|
||||
- `page_size` (number, optional) - 每页数量
|
||||
- `shop_id` (number, optional) - 被分配的店铺ID
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
套餐系列分配的数据模型SHALL重构为更细粒度的佣金配置结构,移除旧的 `base_commission` 模式,采用独立的一次性佣金和强制充值配置字段。
|
||||
|
||||
**字段定义**:
|
||||
|
||||
- `id` (number) - 分配ID
|
||||
- `series_id` (number) - 套餐系列ID
|
||||
- `series_name` (string) - 套餐系列名称
|
||||
@@ -27,6 +28,7 @@
|
||||
- `updated_at` (string) - 更新时间
|
||||
|
||||
**创建请求字段** (`CreateShopSeriesAllocationRequest`):
|
||||
|
||||
- `series_id` (number, required) - 套餐系列ID
|
||||
- `shop_id` (number, required) - 被分配的店铺ID
|
||||
- `one_time_commission_amount` (number, required) - 一次性佣金金额上限(分)
|
||||
@@ -38,6 +40,7 @@
|
||||
- `force_recharge_trigger_type` (number, optional) - 强充触发类型
|
||||
|
||||
**更新请求字段** (`UpdateShopSeriesAllocationRequest`):
|
||||
|
||||
- 所有字段均为可选,允许部分更新
|
||||
- `enable_one_time_commission` (boolean, optional)
|
||||
- `one_time_commission_amount` (number, optional)
|
||||
|
||||
@@ -20,10 +20,7 @@ export class AgentRechargeService extends BaseService {
|
||||
static getAgentRecharges(
|
||||
params?: AgentRechargeQueryParams
|
||||
): Promise<BaseResponse<AgentRechargeListResponse>> {
|
||||
return this.get<BaseResponse<AgentRechargeListResponse>>(
|
||||
'/api/admin/agent-recharges',
|
||||
params
|
||||
)
|
||||
return this.get<BaseResponse<AgentRechargeListResponse>>('/api/admin/agent-recharges', params)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -383,19 +383,19 @@ export class CardService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用 IoT 卡
|
||||
* @param id IoT卡ID
|
||||
* 启用 IoT 卡 单卡复机
|
||||
* @param iccid
|
||||
*/
|
||||
static enableIotCard(id: number): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/iot-cards/${id}/enable`, {})
|
||||
static enableIotCard(iccid: string): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/assets/card/${iccid}/start`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用 IoT 卡
|
||||
* @param id IoT卡ID
|
||||
* 停用 IoT 卡 单卡停机
|
||||
* @param iccid
|
||||
*/
|
||||
static disableIotCard(id: number): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/iot-cards/${id}/disable`, {})
|
||||
static disableIotCard(iccid: string): Promise<BaseResponse> {
|
||||
return this.post<BaseResponse>(`/api/admin/assets/card/${iccid}/stop`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,17 +410,8 @@ export class CardService extends BaseService {
|
||||
* 获取套餐流量详单(每日流量记录)
|
||||
* @param packageUsageId 套餐使用记录ID
|
||||
*/
|
||||
static getPackageDailyRecords(packageUsageId: number): Promise<BaseResponse<{
|
||||
package_name: string
|
||||
package_usage_id: number
|
||||
total_usage_mb: number
|
||||
records: Array<{
|
||||
date: string
|
||||
daily_usage_mb: number
|
||||
cumulative_usage_mb: number
|
||||
}>
|
||||
}>> {
|
||||
return this.get<BaseResponse<{
|
||||
static getPackageDailyRecords(packageUsageId: number): Promise<
|
||||
BaseResponse<{
|
||||
package_name: string
|
||||
package_usage_id: number
|
||||
total_usage_mb: number
|
||||
@@ -429,6 +420,19 @@ export class CardService extends BaseService {
|
||||
daily_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`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export class DeviceService extends BaseService {
|
||||
*/
|
||||
static rebootDevice(imei: string): Promise<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>> {
|
||||
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
|
||||
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||
`/api/admin/devices/by-imei/${imei}/speed-limit`,
|
||||
`/api/admin/devices/by-identifier/${imei}/speed-limit`,
|
||||
data
|
||||
)
|
||||
}
|
||||
@@ -211,7 +211,7 @@ export class DeviceService extends BaseService {
|
||||
data: SwitchCardRequest
|
||||
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||
return this.post<BaseResponse<DeviceOperationResponse>>(
|
||||
`/api/admin/devices/by-imei/${imei}/switch-card`,
|
||||
`/api/admin/devices/by-identifier/${imei}/switch-card`,
|
||||
data
|
||||
)
|
||||
}
|
||||
@@ -226,7 +226,7 @@ export class DeviceService extends BaseService {
|
||||
data: SetWiFiRequest
|
||||
): Promise<BaseResponse<DeviceOperationResponse>> {
|
||||
return this.put<BaseResponse<DeviceOperationResponse>>(
|
||||
`/api/admin/devices/by-imei/${imei}/wifi`,
|
||||
`/api/admin/devices/by-identifier/${imei}/wifi`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ export class ExchangeService extends BaseService {
|
||||
*/
|
||||
static getExchanges(
|
||||
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<
|
||||
BaseResponse<{ list: ExchangeResponse[]; page: number; page_size: number; total: number }>
|
||||
>('/api/admin/exchanges', params)
|
||||
@@ -123,7 +125,10 @@ export class ExchangeService extends BaseService {
|
||||
* @param id 换货单ID
|
||||
* @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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ export class ManualTriggerService extends BaseService {
|
||||
* 批量手动触发
|
||||
* POST /api/admin/polling-manual-trigger/batch
|
||||
*/
|
||||
static batchTrigger(
|
||||
data: BatchManualTriggerRequest
|
||||
): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
static batchTrigger(data: BatchManualTriggerRequest): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
return this.post<BaseResponse<ManualTriggerLog>>(
|
||||
'/api/admin/polling-manual-trigger/batch',
|
||||
data
|
||||
@@ -46,9 +44,7 @@ export class ManualTriggerService extends BaseService {
|
||||
* 单卡手动触发
|
||||
* POST /api/admin/polling-manual-trigger/single
|
||||
*/
|
||||
static singleTrigger(
|
||||
data: SingleManualTriggerRequest
|
||||
): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
static singleTrigger(data: SingleManualTriggerRequest): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
return this.post<BaseResponse<ManualTriggerLog>>(
|
||||
'/api/admin/polling-manual-trigger/single',
|
||||
data
|
||||
|
||||
@@ -51,12 +51,7 @@ export class OrderService extends BaseService {
|
||||
* 套餐购买预检
|
||||
* @param data 预检请求参数
|
||||
*/
|
||||
static purchaseCheck(
|
||||
data: PurchaseCheckRequest
|
||||
): Promise<BaseResponse<PurchaseCheckResponse>> {
|
||||
return this.post<BaseResponse<PurchaseCheckResponse>>(
|
||||
'/api/admin/orders/purchase-check',
|
||||
data
|
||||
)
|
||||
static purchaseCheck(data: PurchaseCheckRequest): Promise<BaseResponse<PurchaseCheckResponse>> {
|
||||
return this.post<BaseResponse<PurchaseCheckResponse>>('/api/admin/orders/purchase-check', data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ export class PollingConcurrencyService extends BaseService {
|
||||
* @param taskType 任务类型
|
||||
* @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)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@ export class ShopSeriesGrantService extends BaseService {
|
||||
* GET /api/admin/shop-series-grants/{id}
|
||||
* @param id 授权ID
|
||||
*/
|
||||
static getShopSeriesGrantDetail(
|
||||
id: number
|
||||
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||
static getShopSeriesGrantDetail(id: number): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||
return this.getOne<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`)
|
||||
}
|
||||
|
||||
@@ -57,10 +55,7 @@ export class ShopSeriesGrantService extends BaseService {
|
||||
id: number,
|
||||
data: UpdateShopSeriesGrantRequest
|
||||
): Promise<BaseResponse<ShopSeriesGrantResponse>> {
|
||||
return this.update<ShopSeriesGrantResponse>(
|
||||
`/api/admin/shop-series-grants/${id}`,
|
||||
data
|
||||
)
|
||||
return this.update<ShopSeriesGrantResponse>(`/api/admin/shop-series-grants/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,9 +32,7 @@ export class WechatConfigService extends BaseService {
|
||||
/**
|
||||
* 创建支付配置
|
||||
*/
|
||||
static createWechatConfig(
|
||||
data: CreateWechatConfigRequest
|
||||
): Promise<BaseResponse<WechatConfig>> {
|
||||
static createWechatConfig(data: CreateWechatConfigRequest): Promise<BaseResponse<WechatConfig>> {
|
||||
return this.post<BaseResponse<WechatConfig>>('/api/admin/wechat-configs', data)
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
ElMessage.success('操作成功')
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
console.log('操作失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -166,7 +166,9 @@
|
||||
label: '用户类型',
|
||||
width: 110,
|
||||
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)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -30,130 +30,130 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElCard } from 'element-plus'
|
||||
import { ElCard } from 'element-plus'
|
||||
|
||||
export interface DetailField {
|
||||
label: string // 字段标签
|
||||
prop?: string // 数据属性路径,支持点号分隔的嵌套路径,如 'one_time_commission_config.enable'
|
||||
formatter?: (value: any, data: any) => string // 自定义格式化函数
|
||||
render?: (data: any) => any // 自定义渲染函数,返回 VNode
|
||||
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 DetailField {
|
||||
label: string // 字段标签
|
||||
prop?: string // 数据属性路径,支持点号分隔的嵌套路径,如 'one_time_commission_config.enable'
|
||||
formatter?: (value: any, data: any) => string // 自定义格式化函数
|
||||
render?: (data: any) => any // 自定义渲染函数,返回 VNode
|
||||
fullWidth?: boolean // 是否占据整行
|
||||
}
|
||||
|
||||
// 默认处理
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
export interface DetailSection {
|
||||
title: string // 分组标题
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-container {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
.detail-container {
|
||||
max-height: 70vh;
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.section-fields {
|
||||
display: grid;
|
||||
gap: 16px 24px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.single-column {
|
||||
grid-template-columns: 1fr;
|
||||
.section-title {
|
||||
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 {
|
||||
&.double-column {
|
||||
display: grid;
|
||||
gap: 16px 24px;
|
||||
|
||||
&.single-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&.double-column {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.field-item {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 32px;
|
||||
|
||||
&.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
flex-shrink: 0;
|
||||
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>
|
||||
|
||||
@@ -255,7 +255,7 @@
|
||||
user-select: none;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -267,8 +267,8 @@
|
||||
.menu-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
<style scoped lang="scss">
|
||||
.table-context-menu-hint {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="设置WiFi" width="500px" @close="handleClose">
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="设备信息">
|
||||
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
|
||||
</ElFormItem>
|
||||
@@ -96,23 +91,29 @@
|
||||
]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// 重置表单
|
||||
formData.enabled = 1
|
||||
formData.ssid = ''
|
||||
formData.password = ''
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// 重置表单
|
||||
formData.enabled = 1
|
||||
formData.ssid = ''
|
||||
formData.password = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
watch(() => props.loading, (val) => {
|
||||
confirmLoading.value = val
|
||||
})
|
||||
watch(
|
||||
() => props.loading,
|
||||
(val) => {
|
||||
confirmLoading.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
@@ -122,7 +123,7 @@
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (valid) {
|
||||
emit('confirm', {
|
||||
enabled: formData.enabled,
|
||||
ssid: formData.ssid,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<ElDialog v-model="visible" title="设置限速" width="500px" @close="handleClose">
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="设备信息">
|
||||
<span style="font-weight: bold; color: #409eff">{{ deviceInfo }}</span>
|
||||
</ElFormItem>
|
||||
@@ -17,7 +12,7 @@
|
||||
controls-position="right"
|
||||
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 label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
@@ -27,7 +22,7 @@
|
||||
controls-position="right"
|
||||
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>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -75,22 +70,28 @@
|
||||
upload_speed: [{ required: true, message: '请输入上行速率', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// 重置表单
|
||||
formData.download_speed = 1024
|
||||
formData.upload_speed = 512
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// 重置表单
|
||||
formData.download_speed = 1024
|
||||
formData.upload_speed = 512
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
watch(() => props.loading, (val) => {
|
||||
confirmLoading.value = val
|
||||
})
|
||||
watch(
|
||||
() => props.loading,
|
||||
(val) => {
|
||||
confirmLoading.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
|
||||
: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>
|
||||
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
|
||||
</div>
|
||||
@@ -123,13 +123,16 @@
|
||||
target_iccid: [{ required: true, message: '请选择目标ICCID', trigger: 'change' }]
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// 重置表单
|
||||
formData.target_iccid = ''
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// 重置表单
|
||||
formData.target_iccid = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
|
||||
@@ -28,9 +28,7 @@ export const useAuth = () => {
|
||||
|
||||
if (Array.isArray(permission)) {
|
||||
// 多个权限(满足任意一个即可)
|
||||
return permission.some(
|
||||
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
|
||||
)
|
||||
return permission.some((perm) => userStore.hasPermission(perm) || userStore.hasButton(perm))
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -45,9 +43,7 @@ export const useAuth = () => {
|
||||
if (!permissions || !Array.isArray(permissions)) return false
|
||||
|
||||
// 需要全部满足
|
||||
return permissions.every(
|
||||
(perm) => userStore.hasPermission(perm) || userStore.hasButton(perm)
|
||||
)
|
||||
return permissions.every((perm) => userStore.hasPermission(perm) || userStore.hasButton(perm))
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -94,7 +94,10 @@ export function useCheckedColumns(columnsFactory: () => ColumnOption[]) {
|
||||
|
||||
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
|
||||
|
||||
// 包装原有的 formatter,在其基础上添加空值处理
|
||||
|
||||
@@ -21,12 +21,7 @@ export function useTableContextMenu() {
|
||||
/**
|
||||
* 单元格鼠标进入事件处理
|
||||
*/
|
||||
const handleCellMouseEnter = (
|
||||
row: any,
|
||||
column: any,
|
||||
cell: HTMLElement,
|
||||
event: MouseEvent
|
||||
) => {
|
||||
const handleCellMouseEnter = (row: any, column: any, cell: HTMLElement, event: MouseEvent) => {
|
||||
// 清除之前的定时器
|
||||
if (hintTimer) {
|
||||
clearTimeout(hintTimer)
|
||||
|
||||
14
src/types/auto-imports.d.ts
vendored
14
src/types/auto-imports.d.ts
vendored
@@ -7,19 +7,19 @@
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||
const ElButton2: typeof import('element-plus/es')['ElButton2']
|
||||
const ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
const ElDropdown2: typeof import('element-plus/es')['ElDropdown2']
|
||||
const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||
const ElButton2: (typeof import('element-plus/es'))['ElButton2']
|
||||
const ElDropdown: (typeof import('element-plus/es'))['ElDropdown']
|
||||
const ElDropdown2: (typeof import('element-plus/es'))['ElDropdown2']
|
||||
const ElDropdownItem: (typeof import('element-plus/es'))['ElDropdownItem']
|
||||
const ElDropdownMenu: (typeof import('element-plus/es'))['ElDropdownMenu']
|
||||
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
|
||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
||||
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
||||
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
||||
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 asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
|
||||
@@ -888,8 +888,8 @@
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.dialog-title {
|
||||
@@ -899,8 +899,8 @@
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.account-name {
|
||||
font-size: 14px;
|
||||
@@ -911,25 +911,25 @@
|
||||
|
||||
.role-transfer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
min-height: 500px;
|
||||
padding: 20px 0;
|
||||
|
||||
.transfer-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: 380px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-width: 380px;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
@@ -943,8 +943,8 @@
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
.role-list {
|
||||
display: flex;
|
||||
@@ -964,9 +964,9 @@
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
@@ -980,9 +980,9 @@
|
||||
|
||||
.assigned-role-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
@@ -999,9 +999,9 @@
|
||||
.transfer-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.el-button {
|
||||
|
||||
@@ -1065,8 +1065,8 @@
|
||||
.card-selection-info {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-color-info);
|
||||
font-size: 14px;
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
|
||||
.card-pagination {
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')" v-permission="'platform_account:add'">新增平台账号</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-permission="'platform_account:add'"
|
||||
>新增平台账号</ElButton
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
@@ -158,18 +160,11 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
|
||||
<div
|
||||
v-for="role in filteredAvailableRoles"
|
||||
:key="role.ID"
|
||||
class="role-item"
|
||||
>
|
||||
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
|
||||
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
|
||||
<span class="role-info">
|
||||
<span>{{ role.role_name }}</span>
|
||||
<ElTag
|
||||
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||
</ElTag>
|
||||
</span>
|
||||
@@ -212,19 +207,11 @@
|
||||
>
|
||||
<span class="role-info">
|
||||
<span>{{ role.role_name }}</span>
|
||||
<ElTag
|
||||
:type="role.role_type === 1 ? 'primary' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
|
||||
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
|
||||
</ElTag>
|
||||
</span>
|
||||
<ElButton
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="removeSingleRole(role.ID)"
|
||||
>
|
||||
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
|
||||
移除
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -956,8 +943,8 @@
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.dialog-title {
|
||||
@@ -967,8 +954,8 @@
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.account-name {
|
||||
font-size: 14px;
|
||||
@@ -979,25 +966,25 @@
|
||||
|
||||
.role-transfer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
min-height: 500px;
|
||||
padding: 20px 0;
|
||||
|
||||
.transfer-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: 380px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-width: 380px;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
@@ -1011,8 +998,8 @@
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
.role-list {
|
||||
display: flex;
|
||||
@@ -1032,9 +1019,9 @@
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
@@ -1048,9 +1035,9 @@
|
||||
|
||||
.assigned-role-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
@@ -1067,9 +1054,9 @@
|
||||
.transfer-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.el-button {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')" v-permission="'shop_account:add'">新增代理账号</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-permission="'shop_account:add'"
|
||||
>新增代理账号</ElButton
|
||||
>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取资产分配详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -107,8 +106,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -122,10 +121,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取分配记录列表失败')
|
||||
console.log('获取分配记录列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -382,8 +382,10 @@
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
switch (
|
||||
item.key
|
||||
// No cases available
|
||||
) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取授权记录详情失败')
|
||||
console.log('获取授权记录详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -112,8 +112,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -127,10 +127,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -343,7 +343,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取授权记录列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -417,7 +416,6 @@
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('备注修改失败')
|
||||
} finally {
|
||||
remarkLoading.value = false
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
|
||||
<!-- 设备详情弹窗 -->
|
||||
<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>
|
||||
</div>
|
||||
<div v-else-if="currentDeviceDetail">
|
||||
@@ -398,14 +398,16 @@
|
||||
</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>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ElTable :data="deviceCards" border style="width: 100%">
|
||||
<ElTableColumn prop="slot_position" label="插槽位置" width="120" align="center">
|
||||
<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>
|
||||
<ElTag v-if="row.is_current" type="success" size="small">当前</ElTag>
|
||||
</div>
|
||||
@@ -435,7 +437,7 @@
|
||||
</ElTable>
|
||||
<div
|
||||
v-if="deviceCards.length === 0"
|
||||
style="text-align: center; padding: 20px; color: #909399"
|
||||
style=" padding: 20px; color: #909399;text-align: center"
|
||||
>
|
||||
暂无设备绑定的卡
|
||||
</div>
|
||||
@@ -451,7 +453,7 @@
|
||||
/>
|
||||
|
||||
<!-- 设置限速对话框 -->
|
||||
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="500px">
|
||||
<ElDialog v-model="speedLimitDialogVisible" title="设置限速" width="50%">
|
||||
<ElForm
|
||||
ref="speedLimitFormRef"
|
||||
:model="speedLimitForm"
|
||||
@@ -469,7 +471,7 @@
|
||||
controls-position="right"
|
||||
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 label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
@@ -479,7 +481,7 @@
|
||||
controls-position="right"
|
||||
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>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -492,7 +494,7 @@
|
||||
|
||||
<!-- 切换SIM卡对话框 -->
|
||||
<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>
|
||||
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||
</div>
|
||||
@@ -534,9 +536,11 @@
|
||||
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}${card.is_current ? ' (当前)' : ''}`"
|
||||
: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>
|
||||
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
|
||||
<ElTag size="small" style="margin-left: 10px"
|
||||
>插槽{{ card.slot_position }}</ElTag
|
||||
>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
@@ -1042,7 +1046,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('解绑卡失败:', error)
|
||||
ElMessage.error(error?.message || '解绑失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1214,13 +1217,15 @@
|
||||
prop: 'last_online_time',
|
||||
label: '最后在线时间',
|
||||
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',
|
||||
label: '最后同步时间',
|
||||
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',
|
||||
@@ -1310,7 +1315,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取设备列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -1368,7 +1372,6 @@
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1416,7 +1419,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('分配失败')
|
||||
} finally {
|
||||
allocateLoading.value = false
|
||||
}
|
||||
@@ -1467,7 +1469,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('回收失败')
|
||||
} finally {
|
||||
recallLoading.value = false
|
||||
}
|
||||
@@ -1510,7 +1511,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取套餐系列列表失败:', error)
|
||||
ElMessage.error('获取套餐系列列表失败')
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
@@ -1711,7 +1711,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('重启设备失败:', error)
|
||||
ElMessage.error(error?.message || '重启失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1740,7 +1739,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('恢复出厂设置失败:', error)
|
||||
ElMessage.error(error?.message || '操作失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1776,7 +1774,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('设置限速失败:', error)
|
||||
ElMessage.error(error?.message || '设置失败')
|
||||
} finally {
|
||||
speedLimitLoading.value = false
|
||||
}
|
||||
@@ -1818,7 +1815,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备绑定卡列表失败:', error)
|
||||
ElMessage.error('加载卡列表失败')
|
||||
} finally {
|
||||
loadingDeviceCards.value = false
|
||||
}
|
||||
@@ -1843,7 +1839,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('切换SIM卡失败:', error)
|
||||
ElMessage.error(error?.message || '切换失败')
|
||||
} finally {
|
||||
switchCardLoading.value = false
|
||||
}
|
||||
@@ -1881,7 +1876,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('设置WiFi失败:', error)
|
||||
ElMessage.error(error?.message || '设置失败')
|
||||
} finally {
|
||||
setWiFiLoading.value = false
|
||||
}
|
||||
@@ -1915,12 +1909,12 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (hasAuth('device:set_speed_limit')) {
|
||||
items.push({
|
||||
key: 'speed-limit',
|
||||
label: '设置限速'
|
||||
})
|
||||
}
|
||||
// if (hasAuth('device:set_speed_limit')) {
|
||||
// items.push({
|
||||
// key: 'speed-limit',
|
||||
// label: '设置限速'
|
||||
// })
|
||||
// }
|
||||
|
||||
if (hasAuth('device:switch_sim')) {
|
||||
items.push({
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询设备详情失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败,请检查设备号是否正确')
|
||||
console.log(error?.message || '查询失败,请检查设备号是否正确')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -506,7 +506,6 @@
|
||||
ElMessage.success('设备导入模板下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载模板失败:', error)
|
||||
ElMessage.error('下载模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +594,6 @@
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('设备导入失败:', error)
|
||||
ElMessage.error(error.message || '设备导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
@@ -645,7 +643,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败数据失败:', error)
|
||||
ElMessage.error('下载失败数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -539,7 +539,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(t('enterpriseDevices.messages.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -607,7 +606,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取设备列表失败')
|
||||
} finally {
|
||||
availableDevicesLoading.value = false
|
||||
}
|
||||
@@ -699,7 +697,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error(t('enterpriseDevices.messages.allocateFailed'))
|
||||
} finally {
|
||||
allocateLoading.value = false
|
||||
}
|
||||
@@ -799,8 +796,8 @@
|
||||
.device-selection-info {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-color-info);
|
||||
font-size: 14px;
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -64,8 +63,7 @@
|
||||
{ label: '换货单号', prop: 'exchange_no' },
|
||||
{
|
||||
label: '状态',
|
||||
render: (data) =>
|
||||
h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
|
||||
render: (data) => h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
|
||||
},
|
||||
{
|
||||
label: '换货原因',
|
||||
@@ -80,11 +78,7 @@
|
||||
{
|
||||
label: '新资产类型',
|
||||
formatter: (_, data) =>
|
||||
data.new_asset_type
|
||||
? data.new_asset_type === 'iot_card'
|
||||
? 'IoT卡'
|
||||
: '设备'
|
||||
: '--'
|
||||
data.new_asset_type ? (data.new_asset_type === 'iot_card' ? 'IoT卡' : '设备') : '--'
|
||||
},
|
||||
{
|
||||
label: '新资产标识符',
|
||||
|
||||
@@ -13,16 +13,9 @@
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<ArtTableHeader v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="showCreateDialog"
|
||||
v-permission="'exchange:create'"
|
||||
>
|
||||
<ElButton type="primary" @click="showCreateDialog" v-permission="'exchange:create'">
|
||||
创建换货单
|
||||
</ElButton>
|
||||
</template>
|
||||
@@ -68,12 +61,7 @@
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleCloseCreateDialog"
|
||||
>
|
||||
<ElForm
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<ElFormItem label="换货原因" prop="exchange_reason">
|
||||
<ElInput
|
||||
v-model="createForm.exchange_reason"
|
||||
@@ -83,7 +71,11 @@
|
||||
/>
|
||||
</ElFormItem>
|
||||
<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="设备" value="device" />
|
||||
</ElSelect>
|
||||
@@ -122,12 +114,7 @@
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleCloseShipDialog"
|
||||
>
|
||||
<ElForm
|
||||
ref="shipFormRef"
|
||||
:model="shipForm"
|
||||
:rules="shipRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="shipFormRef" :model="shipForm" :rules="shipRules" label-width="120px">
|
||||
<ElFormItem label="新资产标识符" prop="new_identifier">
|
||||
<ElInput
|
||||
v-model="shipForm.new_identifier"
|
||||
@@ -142,7 +129,7 @@
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否迁移数据">
|
||||
<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>
|
||||
</ElFormItem>
|
||||
@@ -185,12 +172,8 @@
|
||||
const router = useRouter()
|
||||
|
||||
// 右键菜单相关
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
const { showContextMenuHint, hintPosition, handleCellMouseEnter, handleCellMouseLeave } =
|
||||
useTableContextMenu()
|
||||
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentExchangeRow = ref<ExchangeResponse | null>(null)
|
||||
@@ -334,15 +317,14 @@
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 130,
|
||||
formatter: (row: any) =>
|
||||
h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
||||
formatter: (row: any) => h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.created_at)
|
||||
},
|
||||
}
|
||||
])
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
|
||||
<!-- 卡详情对话框 -->
|
||||
<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>
|
||||
<div style="margin-top: 16px">加载中...</div>
|
||||
</div>
|
||||
@@ -465,7 +465,7 @@
|
||||
|
||||
<!-- 流量使用查询对话框 -->
|
||||
<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>
|
||||
<div style="margin-top: 16px">查询中...</div>
|
||||
</div>
|
||||
@@ -494,7 +494,7 @@
|
||||
|
||||
<!-- 实名状态查询对话框 -->
|
||||
<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>
|
||||
<div style="margin-top: 16px">查询中...</div>
|
||||
</div>
|
||||
@@ -517,7 +517,7 @@
|
||||
|
||||
<!-- 卡实时状态查询对话框 -->
|
||||
<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>
|
||||
<div style="margin-top: 16px">查询中...</div>
|
||||
</div>
|
||||
@@ -1108,7 +1108,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取单卡列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -1364,7 +1363,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('批量回收失败,请重试')
|
||||
} finally {
|
||||
recallLoading.value = false
|
||||
}
|
||||
@@ -1407,7 +1405,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取套餐系列列表失败:', error)
|
||||
ElMessage.error('获取套餐系列列表失败')
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
@@ -1475,7 +1472,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('批量设置套餐系列失败,请重试')
|
||||
} finally {
|
||||
seriesBindingLoading.value = false
|
||||
}
|
||||
@@ -1487,27 +1483,6 @@
|
||||
const moreMenuItems = computed((): 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')) {
|
||||
items.push({
|
||||
key: 'download',
|
||||
@@ -1518,7 +1493,7 @@
|
||||
if (hasAuth('iot_card:change_package')) {
|
||||
items.push({
|
||||
key: 'changePackage',
|
||||
label: '变更套餐'
|
||||
label: '其他功能暂定'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1698,7 +1673,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询流量使用失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败')
|
||||
flowUsageDialogVisible.value = false
|
||||
} finally {
|
||||
flowUsageLoading.value = false
|
||||
@@ -1735,7 +1709,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询实名状态失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败')
|
||||
realnameStatusDialogVisible.value = false
|
||||
} finally {
|
||||
realnameStatusLoading.value = false
|
||||
@@ -1755,7 +1728,7 @@
|
||||
// 直接使用 resolveAsset 返回的网络状态
|
||||
cardStatusData.value = {
|
||||
iccid: iccid,
|
||||
cardStatus: res.data. === 1 ? '正常' : '停机',
|
||||
cardStatus: res.data.network_status === 1 ? '正常' : '停机',
|
||||
extend: res.data.extend
|
||||
}
|
||||
} else {
|
||||
@@ -1764,7 +1737,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询卡状态失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败')
|
||||
cardStatusDialogVisible.value = false
|
||||
} finally {
|
||||
cardStatusLoading.value = false
|
||||
@@ -1841,7 +1813,7 @@
|
||||
const res = await CardService.deactivateIotCard(card.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('手动停用成功')
|
||||
getTableData()
|
||||
await getTableData()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('手动停用失败:', error)
|
||||
|
||||
@@ -131,7 +131,6 @@
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('查询卡片详情失败:', error)
|
||||
ElMessage.error(error?.message || '查询失败,请检查ICCID是否正确')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
align-center
|
||||
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 />
|
||||
</div>
|
||||
<div v-else-if="failedItems.length > 0">
|
||||
@@ -92,7 +92,7 @@
|
||||
<ElTableColumn prop="message" label="失败原因" min-width="200" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</div>
|
||||
<div v-else style="text-align: center; padding: 40px">
|
||||
<div v-else style=" padding: 40px;text-align: center">
|
||||
<ElEmpty description="暂无失败数据" />
|
||||
</div>
|
||||
|
||||
@@ -555,7 +555,6 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取失败数据失败:', error)
|
||||
ElMessage.error('获取失败数据失败')
|
||||
} finally {
|
||||
failDataLoading.value = false
|
||||
}
|
||||
@@ -594,7 +593,6 @@
|
||||
ElMessage.success('失败数据下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败数据失败:', error)
|
||||
ElMessage.error('下载失败数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +651,6 @@
|
||||
ElMessage.success('IoT卡导入模板下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载模板失败:', error)
|
||||
ElMessage.error('下载模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,7 +771,6 @@
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('IoT卡导入失败:', error)
|
||||
ElMessage.error(error.message || 'IoT卡导入失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
console.log('获取任务详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -252,8 +252,8 @@
|
||||
.task-detail-page {
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
|
||||
@@ -115,15 +115,7 @@
|
||||
const { isDark, systemThemeType } = storeToRefs(settingStore)
|
||||
|
||||
// 使用登录 Composable
|
||||
const {
|
||||
formRef,
|
||||
formData,
|
||||
rules,
|
||||
loading,
|
||||
mockAccounts,
|
||||
setupAccount,
|
||||
handleLogin
|
||||
} = useLogin()
|
||||
const { formRef, formData, rules, loading, mockAccounts, setupAccount, handleLogin } = useLogin()
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
|
||||
|
||||
@@ -644,7 +644,7 @@
|
||||
// 刷新列表
|
||||
getMyCardsList()
|
||||
} catch (error) {
|
||||
ElMessage.error('分销操作失败,请重试')
|
||||
console.log('分销操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +660,7 @@
|
||||
// 刷新列表
|
||||
getMyCardsList()
|
||||
} catch (error) {
|
||||
ElMessage.error('充值操作失败,请重试')
|
||||
console.log('充值操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,7 +677,7 @@
|
||||
tableData.value = tableData.value.filter((item) => !recycledIds.includes(item.id))
|
||||
selectedRows.value = []
|
||||
} catch (error) {
|
||||
ElMessage.error('回收操作失败,请重试')
|
||||
console.log('回收操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -679,8 +679,8 @@
|
||||
.account-list-page {
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -694,8 +694,8 @@
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.dialog-title {
|
||||
@@ -705,8 +705,8 @@
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.account-name {
|
||||
font-size: 14px;
|
||||
@@ -717,25 +717,25 @@
|
||||
|
||||
.role-transfer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
min-height: 500px;
|
||||
padding: 20px 0;
|
||||
|
||||
.transfer-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: 380px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-width: 380px;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
@@ -749,8 +749,8 @@
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
.role-list {
|
||||
display: flex;
|
||||
@@ -772,15 +772,15 @@
|
||||
width: 100%;
|
||||
|
||||
.el-checkbox__label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
> span:first-child {
|
||||
@@ -794,16 +794,16 @@
|
||||
|
||||
.assigned-role-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
> span:first-child {
|
||||
flex: 1;
|
||||
@@ -825,9 +825,9 @@
|
||||
.transfer-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.el-button {
|
||||
|
||||
@@ -132,10 +132,10 @@
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
@@ -220,7 +220,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.active-wechat-config-widget {
|
||||
.card-header {
|
||||
.header-left,
|
||||
|
||||
@@ -151,8 +151,8 @@
|
||||
|
||||
.stats-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card-wrapper {
|
||||
@@ -171,6 +171,7 @@
|
||||
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
@@ -178,7 +179,6 @@
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
@@ -193,10 +193,10 @@
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
overflow: hidden;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -204,7 +204,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
@media (width <= 1400px) {
|
||||
.commission-summary-widget {
|
||||
.stats-container {
|
||||
gap: 12px;
|
||||
@@ -224,7 +224,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@media (width <= 1200px) {
|
||||
.commission-summary-widget {
|
||||
.stat-card-wrapper {
|
||||
flex: 1 1 calc(33.333% - 12px);
|
||||
@@ -233,7 +233,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.commission-summary-widget {
|
||||
.stats-container {
|
||||
gap: 12px;
|
||||
@@ -246,7 +246,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (width <= 480px) {
|
||||
.commission-summary-widget {
|
||||
.stat-card-wrapper {
|
||||
flex: 1 1 100%;
|
||||
|
||||
@@ -117,10 +117,10 @@
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
@@ -205,7 +205,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.withdrawal-settings-widget {
|
||||
.card-header {
|
||||
.header-left,
|
||||
|
||||
@@ -32,11 +32,7 @@
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { AgentRechargeService } from '@/api/modules'
|
||||
import type {
|
||||
AgentRecharge,
|
||||
AgentRechargeStatus,
|
||||
AgentRechargePaymentMethod
|
||||
} from '@/types/api'
|
||||
import type { AgentRecharge, AgentRechargeStatus, AgentRechargePaymentMethod } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
@@ -100,7 +96,8 @@
|
||||
},
|
||||
{
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.detail-title {
|
||||
@@ -209,10 +206,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -259,21 +259,14 @@
|
||||
>
|
||||
<ElAlert
|
||||
:title="
|
||||
resolveAction === 'release'
|
||||
? '确认将该笔待审佣金记录入账'
|
||||
: '确认将该笔待审佣金记录作废'
|
||||
resolveAction === 'release' ? '确认将该笔待审佣金记录入账' : '确认将该笔待审佣金记录作废'
|
||||
"
|
||||
:type="resolveAction === 'release' ? 'success' : 'warning'"
|
||||
style="margin-bottom: 16px"
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<ElForm
|
||||
ref="resolveFormRef"
|
||||
:model="resolveForm"
|
||||
:rules="resolveRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElForm ref="resolveFormRef" :model="resolveForm" :rules="resolveRules" label-width="100px">
|
||||
<ElFormItem v-if="resolveAction === 'release'" label="入账金额" prop="amount">
|
||||
<ElInputNumber
|
||||
v-model="resolveForm.amount"
|
||||
@@ -284,7 +277,7 @@
|
||||
style="width: 100%"
|
||||
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分
|
||||
</div>
|
||||
</ElFormItem>
|
||||
@@ -736,7 +729,10 @@
|
||||
}
|
||||
|
||||
// 处理佣金修正
|
||||
const handleResolveCommission = (row: ShopCommissionRecordItem, action: CommissionResolveAction) => {
|
||||
const handleResolveCommission = (
|
||||
row: ShopCommissionRecordItem,
|
||||
action: CommissionResolveAction
|
||||
) => {
|
||||
currentCommissionId.value = row.id
|
||||
resolveAction.value = action
|
||||
resolveForm.amount = undefined
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<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>
|
||||
</ArtTableHeader>
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<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>
|
||||
</ArtTableHeader>
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="实际退款金额">
|
||||
{{
|
||||
refund.approved_refund_amount ? formatCurrency(refund.approved_refund_amount) : '-'
|
||||
}}
|
||||
{{ refund.approved_refund_amount ? formatCurrency(refund.approved_refund_amount) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实收金额">
|
||||
{{ formatCurrency(refund.actual_received_amount) }}
|
||||
@@ -95,12 +93,7 @@
|
||||
width="500px"
|
||||
@closed="handleApproveDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="approveFormRef"
|
||||
:model="approveForm"
|
||||
:rules="approveRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
@@ -341,9 +334,7 @@
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (
|
||||
status: RefundStatus
|
||||
): 'warning' | 'success' | 'danger' | 'info' => {
|
||||
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
|
||||
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
1: 'warning', // 待审批
|
||||
2: 'success', // 已通过
|
||||
@@ -551,15 +542,15 @@
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -183,12 +183,7 @@
|
||||
width="500px"
|
||||
@closed="handleRejectDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="rejectFormRef"
|
||||
:model="rejectForm"
|
||||
:rules="rejectRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ currentRefund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
@@ -220,12 +215,7 @@
|
||||
width="500px"
|
||||
@closed="handleReturnDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="returnFormRef"
|
||||
:model="returnForm"
|
||||
:rules="returnRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ currentRefund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
@@ -460,9 +450,7 @@
|
||||
|
||||
const createRules = reactive<FormRules>({
|
||||
order_id: [{ required: true, message: '请输入订单ID', trigger: 'blur' }],
|
||||
requested_refund_amount: [
|
||||
{ required: true, message: '请输入申请退款金额', trigger: 'blur' }
|
||||
],
|
||||
requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }],
|
||||
actual_received_amount: [{ required: true, message: '请输入实收金额', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
@@ -519,9 +507,7 @@
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (
|
||||
status: RefundStatus
|
||||
): 'warning' | 'success' | 'danger' | 'info' => {
|
||||
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
|
||||
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
1: 'warning', // 待审批
|
||||
2: 'success', // 已通过
|
||||
@@ -998,10 +984,7 @@
|
||||
}
|
||||
|
||||
// 已拒绝或已退回状态可以重新提交
|
||||
if (
|
||||
currentOperatingRefund.value.status === 3 ||
|
||||
currentOperatingRefund.value.status === 4
|
||||
) {
|
||||
if (currentOperatingRefund.value.status === 3 || currentOperatingRefund.value.status === 4) {
|
||||
items.push({
|
||||
key: 'resubmit',
|
||||
label: '重新提交'
|
||||
|
||||
@@ -32,13 +32,19 @@
|
||||
type="primary"
|
||||
@click="handleSearchCard"
|
||||
:loading="loading"
|
||||
:icon="Search"
|
||||
style="margin-left: 16px"
|
||||
>查询</ElButton
|
||||
>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<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>
|
||||
</div>
|
||||
@@ -60,7 +66,7 @@
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptions :column="4" border>
|
||||
<!--<ElDescriptionsItem label="资产状态">-->
|
||||
<!-- <ElTag :type="getAssetStatusType(cardInfo?.status)" size="small">-->
|
||||
<!-- {{ getAssetStatusName(cardInfo?.status) }}-->
|
||||
@@ -70,10 +76,9 @@
|
||||
<!-- 卡专属字段 -->
|
||||
<template v-if="cardInfo?.asset_type === 'card'">
|
||||
<ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运营商账户">暂未接入</ElDescriptionsItem>
|
||||
<!--<ElDescriptionsItem label="运营商">{{-->
|
||||
<!-- cardInfo?.carrier_name || '--'-->
|
||||
<!-- }}</ElDescriptionsItem>-->
|
||||
<ElDescriptionsItem label="运营商账户">{{
|
||||
cardInfo?.carrier_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实名状态">
|
||||
<ElTag :type="getRealNameStatusType(cardInfo?.real_name_status)" size="small">
|
||||
{{ getRealNameStatusName(cardInfo?.real_name_status) }}
|
||||
@@ -83,6 +88,9 @@
|
||||
<ElDescriptionsItem label="店铺">{{
|
||||
cardInfo?.shop_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐系列">{{
|
||||
cardInfo?.series_name || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="虚拟号">{{
|
||||
cardInfo?.virtual_no || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
@@ -155,7 +163,11 @@
|
||||
cardInfo?.activated_at || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实名时间">暂未接入</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="信号强度">暂未接入</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="信号强度">
|
||||
<ElTag :type="getSignalStrengthType()" size="small">
|
||||
{{ calculateSignalStrength() }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="在线状态">
|
||||
<ElTag :type="getOnlineStatusType(cardInfo?.online_status)" size="small">
|
||||
@@ -205,21 +217,22 @@
|
||||
<ElTable
|
||||
v-if="cardInfo.cards && cardInfo.cards.length > 0"
|
||||
:data="cardInfo.cards"
|
||||
border
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<ElTableColumn prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn prop="msisdn" label="MSISDN" min-width="120" />
|
||||
<ElTableColumn prop="slot_position" label="卡槽位置" width="120" align="center">
|
||||
<ElTableColumn prop="iccid" label="ICCID" />
|
||||
<ElTableColumn prop="msisdn" label="MSISDN" />
|
||||
<ElTableColumn prop="slot_position" label="卡槽位置" align="center">
|
||||
<template #default="scope">
|
||||
<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>
|
||||
<ElTag v-if="scope.row.is_current" type="success" size="small">当前</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="网络状态" width="100" align="center">
|
||||
<ElTableColumn label="网络状态" align="center">
|
||||
<template #default="scope">
|
||||
<ElTag
|
||||
:type="scope.row.network_status === 1 ? 'success' : 'danger'"
|
||||
@@ -229,7 +242,7 @@
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="实名状态" width="100" align="center">
|
||||
<ElTableColumn label="实名状态" align="center">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getRealNameStatusType(scope.row.real_name_status)" size="small">
|
||||
{{ getRealNameStatusName(scope.row.real_name_status) }}
|
||||
@@ -242,7 +255,7 @@
|
||||
<!-- 设备实时信息 -->
|
||||
<ElDivider content-position="left">设备实时信息</ElDivider>
|
||||
<div v-if="deviceRealtime" style="margin-top: 16px">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptions :column="4" border>
|
||||
<!-- 基本状态 -->
|
||||
<ElDescriptionsItem label="在线状态">
|
||||
<ElTag
|
||||
@@ -299,8 +312,8 @@
|
||||
v-if="deviceRealtime.current_iccid"
|
||||
style="
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
"
|
||||
@click="handleViewCardDetail(deviceRealtime.current_iccid)"
|
||||
>
|
||||
@@ -426,13 +439,13 @@
|
||||
<ElButton type="warning" @click="handleResetDevice" :loading="resetDeviceLoading">
|
||||
恢复出厂
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="info"
|
||||
@click="handleShowSpeedLimitDialog"
|
||||
:loading="speedLimitLoading"
|
||||
>
|
||||
设置限速
|
||||
</ElButton>
|
||||
<!--<ElButton-->
|
||||
<!-- type="info"-->
|
||||
<!-- @click="handleShowSpeedLimitDialog"-->
|
||||
<!-- :loading="speedLimitLoading"-->
|
||||
<!-->-->
|
||||
<!-- 设置限速-->
|
||||
<!--</ElButton>-->
|
||||
<ElButton
|
||||
type="success"
|
||||
@click="handleShowSwitchCardDialog"
|
||||
@@ -470,7 +483,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="cardInfo.currentPackageDetail">
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptions :column="4" border>
|
||||
<!-- 套餐基本信息 -->
|
||||
<ElDescriptionsItem label="套餐名称" :span="1">{{
|
||||
cardInfo.currentPackageDetail.package_name || '--'
|
||||
@@ -524,11 +537,6 @@
|
||||
<ElDescriptionsItem label="创建时间">{{
|
||||
formatDateTime(cardInfo.currentPackageDetail.created_at)
|
||||
}}</ElDescriptionsItem>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<ElDescriptionsItem label="主套餐ID" :span="2">{{
|
||||
cardInfo.currentPackageDetail.master_usage_id || '--'
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
@@ -573,40 +581,10 @@
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:归属信息和钱包概况 -->
|
||||
<div class="row two-columns">
|
||||
<!-- 左侧:归属信息 -->
|
||||
<div class="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 || '--'-->
|
||||
<!--}}</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 || '--'-->
|
||||
<!-- }}</ElDescriptionsItem>-->
|
||||
<!--</template>-->
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:钱包概况 -->
|
||||
<div class="col">
|
||||
<!-- 第三行:钱包概况和套餐列表 -->
|
||||
<div class="row two-columns wallet-package-row">
|
||||
<!-- 左侧:钱包概况 -->
|
||||
<div class="col wallet-col">
|
||||
<ElCard shadow="never" class="info-card wallet-overview-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
@@ -647,28 +625,6 @@
|
||||
</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>
|
||||
<ElEmpty
|
||||
v-else-if="!walletLoading"
|
||||
@@ -677,123 +633,129 @@
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 套餐列表 -->
|
||||
<div class="row full-width">
|
||||
<ElCard shadow="never" class="info-card package-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>套餐列表</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="package-table-wrapper">
|
||||
<ElTable
|
||||
v-if="cardInfo.packageList && cardInfo.packageList.length > 0"
|
||||
:data="cardInfo.packageList"
|
||||
class="package-table"
|
||||
border
|
||||
>
|
||||
<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
|
||||
<!-- 右侧:套餐列表 -->
|
||||
<div class="col package-col">
|
||||
<ElCard shadow="never" class="info-card package-info">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>套餐列表</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="package-table-wrapper">
|
||||
<ElTable
|
||||
v-if="cardInfo.packageList && cardInfo.packageList.length > 0"
|
||||
:data="cardInfo.packageList"
|
||||
class="package-table"
|
||||
border
|
||||
max-height="400"
|
||||
>
|
||||
<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>
|
||||
<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="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>
|
||||
|
||||
<!-- 第六行:钱包流水列表 -->
|
||||
@@ -955,7 +917,7 @@
|
||||
controls-position="right"
|
||||
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 label="上行速率" prop="upload_speed">
|
||||
<ElInputNumber
|
||||
@@ -965,7 +927,7 @@
|
||||
controls-position="right"
|
||||
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>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -982,7 +944,7 @@
|
||||
|
||||
<!-- 切换SIM卡对话框 -->
|
||||
<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>
|
||||
<div style="margin-top: 16px">加载设备绑定的卡列表中...</div>
|
||||
</div>
|
||||
@@ -1026,7 +988,7 @@
|
||||
:label="`${card.iccid} - 插槽${card.slot_position} - ${card.carrier_name}`"
|
||||
: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>
|
||||
<ElTag size="small" style="margin-left: 10px">插槽{{ card.slot_position }}</ElTag>
|
||||
</div>
|
||||
@@ -1117,7 +1079,7 @@
|
||||
</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>
|
||||
<ElRadioGroup v-model="dateFilterType" @change="handleDateFilterChange">
|
||||
<ElRadio value="all">全部</ElRadio>
|
||||
@@ -1183,7 +1145,7 @@
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</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="暂无流量详单数据" />
|
||||
</div>
|
||||
</ElDialog>
|
||||
@@ -1221,7 +1183,7 @@
|
||||
ElInput
|
||||
} 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 { EnterpriseService } from '@/api/modules/enterprise'
|
||||
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查看卡详情
|
||||
const handleViewCardDetail = (iccid: string) => {
|
||||
if (!iccid) return
|
||||
@@ -2295,7 +2301,7 @@
|
||||
})
|
||||
|
||||
enableCardLoading.value = true
|
||||
const res = await CardService.enableIotCard(cardInfo.value.asset_id)
|
||||
const res = await CardService.enableIotCard(cardInfo.value.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('启用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2318,7 +2324,7 @@
|
||||
})
|
||||
|
||||
disableCardLoading.value = true
|
||||
const res = await CardService.disableIotCard(cardInfo.value.asset_id)
|
||||
const res = await CardService.disableIotCard(cardInfo.value.iccid)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('停用成功')
|
||||
await handleRefreshAsset()
|
||||
@@ -2380,7 +2386,6 @@
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('重启失败:', error)
|
||||
ElMessage.error(error?.message || '重启失败')
|
||||
}
|
||||
} finally {
|
||||
rebootDeviceLoading.value = false
|
||||
@@ -2405,7 +2410,6 @@
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('恢复出厂失败:', error)
|
||||
ElMessage.error(error?.message || '恢复出厂失败')
|
||||
}
|
||||
} finally {
|
||||
resetDeviceLoading.value = false
|
||||
@@ -2560,7 +2564,6 @@
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('手动停用失败:', error)
|
||||
ElMessage.error(error?.message || '手动停用失败')
|
||||
}
|
||||
} finally {
|
||||
manualDeactivateDeviceLoading.value = false
|
||||
@@ -2672,9 +2675,9 @@
|
||||
|
||||
.iccid-search {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
overflow: visible;
|
||||
|
||||
.iccid-input-wrapper {
|
||||
@@ -2686,8 +2689,8 @@
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
white-space: nowrap;
|
||||
|
||||
// 三角箭头 - 上边框样式
|
||||
.magnifier-arrow {
|
||||
@@ -2706,6 +2709,7 @@
|
||||
.magnifier-content {
|
||||
display: inline-block;
|
||||
padding: 16px 24px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
@@ -2716,8 +2720,7 @@
|
||||
background-color: var(--el-bg-color, #fff);
|
||||
border: 2px solid var(--el-color-primary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2738,7 +2741,7 @@
|
||||
}
|
||||
|
||||
// 移动端
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
padding: 16px;
|
||||
|
||||
.search-card {
|
||||
@@ -2827,6 +2830,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 钱包和套餐两栏在小屏幕也改为垂直布局
|
||||
.wallet-package-row {
|
||||
flex-direction: column;
|
||||
|
||||
.wallet-col,
|
||||
.package-col {
|
||||
flex: 1;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
@@ -2858,8 +2872,8 @@
|
||||
}
|
||||
|
||||
.package-series-value {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
i {
|
||||
@@ -2882,8 +2896,8 @@
|
||||
}
|
||||
|
||||
.traffic-progress-wrapper {
|
||||
margin-top: 24px;
|
||||
padding: 20px;
|
||||
margin-top: 24px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -2908,19 +2922,19 @@
|
||||
|
||||
.flow-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
background: var(--el-bg-color, #fff);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
@@ -2987,19 +3001,19 @@
|
||||
|
||||
.progress-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
@@ -3031,13 +3045,13 @@
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
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-table-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.package-table {
|
||||
:deep(.el-table__header) {
|
||||
th {
|
||||
@@ -3073,66 +3119,66 @@
|
||||
.wallet-balance-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
gap: 14px;
|
||||
|
||||
.balance-card {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
padding: 18px 20px;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.balance-icon {
|
||||
font-size: 28px;
|
||||
flex-shrink: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
|
||||
.balance-label {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.total {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
.balance-label,
|
||||
.balance-value {
|
||||
color: #fff;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.available {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
|
||||
.balance-label,
|
||||
.balance-value {
|
||||
color: #fff;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
|
||||
&.frozen {
|
||||
background: var(--el-fill-color-light);
|
||||
.balance-value {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3142,10 +3188,10 @@
|
||||
.wallet-info {
|
||||
.wallet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
@@ -3155,9 +3201,9 @@
|
||||
|
||||
.filter-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3172,10 +3218,10 @@
|
||||
|
||||
.reference-no-link {
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
{
|
||||
label: '实付金额',
|
||||
prop: 'actual_paid_amount',
|
||||
formatter: (value) => (value !== undefined && value !== null ? formatCurrency(value) : '-')
|
||||
formatter: (value) =>
|
||||
value !== undefined && value !== null ? formatCurrency(value) : '-'
|
||||
},
|
||||
{
|
||||
label: 'IoT卡ID',
|
||||
@@ -157,7 +158,8 @@
|
||||
},
|
||||
{
|
||||
label: '订单渠道',
|
||||
formatter: (_, data) => (data.purchase_role ? getPurchaseRoleText(data.purchase_role) : '-')
|
||||
formatter: (_, data) =>
|
||||
data.purchase_role ? getPurchaseRoleText(data.purchase_role) : '-'
|
||||
},
|
||||
{
|
||||
label: '购买备注',
|
||||
@@ -256,8 +258,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -269,12 +271,12 @@
|
||||
}
|
||||
|
||||
.order-items-section {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
margin-top: 30px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
@@ -284,10 +286,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<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 || '无接入号' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -130,7 +130,7 @@
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -163,7 +163,7 @@
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<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) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -259,8 +259,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -274,10 +274,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -61,11 +61,11 @@
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增套餐' : '编辑套餐'"
|
||||
width="60%"
|
||||
width="45%"
|
||||
:close-on-click-modal="false"
|
||||
@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">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="套餐编码" prop="package_code">
|
||||
@@ -109,7 +109,7 @@
|
||||
:value="series.id"
|
||||
/>
|
||||
<template #empty>
|
||||
<div style="padding: 10px; text-align: center; color: #909399">
|
||||
<div style="padding: 10px; color: #909399; text-align: center">
|
||||
暂无套餐系列,请先创建套餐系列
|
||||
</div>
|
||||
</template>
|
||||
@@ -182,17 +182,17 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 流量重置天数(calendar_type为by_day时显示) -->
|
||||
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'">
|
||||
<!-- 套餐天数(流量重置周期为每日,或calendar_type为by_day时显示) -->
|
||||
<ElRow :gutter="20" v-if="form.data_reset_cycle === 'daily' || form.calendar_type === 'by_day'">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="流量重置天数" prop="duration_days">
|
||||
<ElFormItem label="套餐天数" prop="duration_days">
|
||||
<ElInputNumber
|
||||
v-model="form.duration_days"
|
||||
:min="1"
|
||||
:max="3650"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
placeholder="请输入流量重置天数"
|
||||
placeholder="请输入套餐天数"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
@@ -341,7 +341,11 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="retailPriceDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirmUpdateRetailPrice" :loading="retailPriceLoading">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleConfirmUpdateRetailPrice"
|
||||
:loading="retailPriceLoading"
|
||||
>
|
||||
确认修改
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -547,13 +551,25 @@
|
||||
cost_price: [{ required: true, message: '请输入成本价', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 如果启用虚流量,则虚流量额度为必填
|
||||
// 如果启用虚流量,则虚流量额度为必填,并且不能超过真流量额度
|
||||
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 = [
|
||||
{ required: true, message: '请输入套餐天数', 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(
|
||||
() => form.data_reset_cycle,
|
||||
(cycle) => {
|
||||
// 不是每月时清空套餐周期类型
|
||||
if (cycle !== 'monthly') {
|
||||
form.calendar_type = undefined
|
||||
}
|
||||
// 不是每日且calendar_type不是按天时,清空套餐天数
|
||||
if (cycle !== 'daily' && form.calendar_type !== 'by_day') {
|
||||
form.duration_days = undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听套餐周期类型变化,不是按天时清空套餐天数
|
||||
// 监听套餐周期类型变化,不是按天且流量重置周期不是每日时清空套餐天数
|
||||
watch(
|
||||
() => form.calendar_type,
|
||||
(type) => {
|
||||
if (type !== 'by_day') {
|
||||
if (type !== 'by_day' && form.data_reset_cycle !== 'daily') {
|
||||
form.duration_days = undefined
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1066,8 @@
|
||||
if (form.data_reset_cycle === 'monthly' && 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
|
||||
}
|
||||
if (suggestedRetailPriceInCents !== undefined) {
|
||||
|
||||
@@ -244,8 +244,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -259,10 +259,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -597,8 +597,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
@@ -619,8 +619,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
@@ -642,10 +642,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增代理系列授权' : '编辑代理系列授权'"
|
||||
width="60%"
|
||||
width="45%"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
@@ -2184,8 +2184,8 @@
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
margin: 20px 0 16px;
|
||||
padding-bottom: 8px;
|
||||
margin: 20px 0 16px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
|
||||
.title-text {
|
||||
@@ -2197,8 +2197,8 @@
|
||||
|
||||
.commission-type-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.type-hint {
|
||||
font-size: 13px;
|
||||
@@ -2252,8 +2252,8 @@
|
||||
|
||||
.cost-price-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.min-cost-hint {
|
||||
font-size: 12px;
|
||||
@@ -2282,8 +2282,8 @@
|
||||
.info-label {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
@@ -2294,8 +2294,8 @@
|
||||
|
||||
.readonly-value {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.package-list-dialog-content {
|
||||
|
||||
@@ -285,8 +285,8 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.alert-history-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,11 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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="critical" />
|
||||
</el-select>
|
||||
@@ -155,188 +159,33 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick, watch, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { PollingAlertService } from '@/api/modules'
|
||||
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ref, reactive, computed, nextTick, watch, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { PollingAlertService } from '@/api/modules'
|
||||
import type { PollingAlertRule, CreatePollingAlertRuleRequest } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingAlertRule[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref()
|
||||
const currentRow = ref<PollingAlertRule | null>(null)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingAlertRule[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref()
|
||||
const currentRow = ref<PollingAlertRule | null>(null)
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
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,
|
||||
const form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
|
||||
rule_name: '',
|
||||
alert_level: '',
|
||||
metric_type: '',
|
||||
@@ -346,134 +195,287 @@ const resetForm = () => {
|
||||
cooldown_minutes: 5,
|
||||
notify_channels: ''
|
||||
})
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
resetForm()
|
||||
dialogType.value = 'create'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
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: '删除' }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
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) {
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
loadData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row: PollingAlertRule) => {
|
||||
ElMessageBox.confirm('确定要删除该告警规则吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
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) {
|
||||
ElMessage.success('删除成功')
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
loadData()
|
||||
}
|
||||
} 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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.alert-rules-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.alert-rules-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,12 +71,7 @@
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="任务类型">
|
||||
<el-input v-model="currentRow.task_type_name" disabled />
|
||||
</el-form-item>
|
||||
@@ -99,194 +94,183 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PollingConcurrencyService } from '@/api/modules'
|
||||
import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PollingConcurrencyService } from '@/api/modules'
|
||||
import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingConcurrency[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentClickRow = ref<PollingConcurrency | null>(null)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<PollingConcurrency[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentClickRow = ref<PollingConcurrency | null>(null)
|
||||
|
||||
const currentRow = ref<PollingConcurrency>({
|
||||
task_type: '',
|
||||
task_type_name: '',
|
||||
max_concurrency: 0,
|
||||
current: 0,
|
||||
available: 0,
|
||||
utilization: 0
|
||||
})
|
||||
const currentRow = ref<PollingConcurrency>({
|
||||
task_type: '',
|
||||
task_type_name: '',
|
||||
max_concurrency: 0,
|
||||
current: 0,
|
||||
available: 0,
|
||||
utilization: 0
|
||||
})
|
||||
|
||||
const formData = reactive<UpdateConcurrencyParams>({
|
||||
max_concurrency: 100
|
||||
})
|
||||
const formData = reactive<UpdateConcurrencyParams>({
|
||||
max_concurrency: 100
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
max_concurrency: [
|
||||
{ required: true, message: '请输入最大并发数', 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 formRules: FormRules = {
|
||||
max_concurrency: [
|
||||
{ required: true, message: '请输入最大并发数', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 1000, message: '并发数范围为 1-1000', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getUtilizationColor = (percentage: number) => {
|
||||
if (percentage >= 90) return '#f56c6c'
|
||||
if (percentage >= 70) return '#e6a23c'
|
||||
if (percentage >= 50) return '#409eff'
|
||||
return '#67c23a'
|
||||
}
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await PollingConcurrencyService.getConcurrencyList()
|
||||
tableData.value = data.items
|
||||
} catch (error) {
|
||||
ElMessage.error('加载并发配置失败')
|
||||
} finally {
|
||||
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()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('重置失败')
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => [
|
||||
{
|
||||
label: '修改配置',
|
||||
key: 'edit',
|
||||
permission: 'polling_concurrency_update'
|
||||
},
|
||||
{
|
||||
label: '重置',
|
||||
key: 'reset',
|
||||
permission: 'polling_concurrency_reset'
|
||||
}
|
||||
])
|
||||
|
||||
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 {
|
||||
await PollingConcurrencyService.updateConcurrency(
|
||||
currentRow.value.task_type,
|
||||
formData
|
||||
)
|
||||
ElMessage.success('修改成功')
|
||||
dialogVisible.value = false
|
||||
const { data } = await PollingConcurrencyService.getConcurrencyList()
|
||||
tableData.value = data.items
|
||||
} catch (error) {
|
||||
ElMessage.error('加载并发配置失败')
|
||||
} finally {
|
||||
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()
|
||||
} 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(() => {
|
||||
loadData()
|
||||
})
|
||||
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
|
||||
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -697,8 +697,8 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.polling-config-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
:total="logPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
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"
|
||||
@current-change="getCleanupLogs"
|
||||
/>
|
||||
@@ -619,7 +619,7 @@
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -86,7 +86,11 @@
|
||||
>
|
||||
<el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="120px">
|
||||
<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:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
@@ -261,7 +265,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
@@ -278,277 +286,62 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules'
|
||||
import type {
|
||||
ManualTriggerLog,
|
||||
ManualTriggerHistoryQueryParams,
|
||||
BatchManualTriggerRequest,
|
||||
ConditionManualTriggerRequest,
|
||||
SingleManualTriggerRequest,
|
||||
TaskType
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ManualTriggerService, CarrierService, ShopService, CardService } from '@/api/modules'
|
||||
import type {
|
||||
ManualTriggerLog,
|
||||
ManualTriggerHistoryQueryParams,
|
||||
BatchManualTriggerRequest,
|
||||
ConditionManualTriggerRequest,
|
||||
SingleManualTriggerRequest,
|
||||
TaskType
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<ManualTriggerLog[]>([])
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref()
|
||||
const currentClickRow = ref<ManualTriggerLog | null>(null)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<ManualTriggerLog[]>([])
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref()
|
||||
const currentClickRow = ref<ManualTriggerLog | null>(null)
|
||||
|
||||
const batchDialogVisible = ref(false)
|
||||
const conditionDialogVisible = ref(false)
|
||||
const singleDialogVisible = ref(false)
|
||||
const batchDialogVisible = ref(false)
|
||||
const conditionDialogVisible = ref(false)
|
||||
const singleDialogVisible = ref(false)
|
||||
|
||||
const batchFormRef = ref<FormInstance>()
|
||||
const conditionFormRef = ref<FormInstance>()
|
||||
const singleFormRef = ref<FormInstance>()
|
||||
const batchFormRef = ref<FormInstance>()
|
||||
const conditionFormRef = ref<FormInstance>()
|
||||
const singleFormRef = ref<FormInstance>()
|
||||
|
||||
const carrierList = ref<any[]>([])
|
||||
const shopTreeData = ref<any[]>([])
|
||||
const cardList = ref<any[]>([])
|
||||
const carrierList = ref<any[]>([])
|
||||
const shopTreeData = ref<any[]>([])
|
||||
const cardList = ref<any[]>([])
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
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
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
task_type: undefined as TaskType | undefined
|
||||
})
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
const showBatchTriggerDialog = () => {
|
||||
Object.assign(batchForm, {
|
||||
task_type: '',
|
||||
// 批量触发表单
|
||||
const batchForm = reactive({
|
||||
task_type: '' as TaskType | '',
|
||||
card_ids_text: ''
|
||||
})
|
||||
batchDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
batchFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const showConditionTriggerDialog = () => {
|
||||
Object.assign(conditionForm, {
|
||||
task_type: '',
|
||||
// 条件触发表单
|
||||
const conditionForm = reactive<ConditionManualTriggerRequest>({
|
||||
task_type: '' as TaskType | '',
|
||||
limit: 100,
|
||||
card_status: undefined,
|
||||
card_type: undefined,
|
||||
@@ -557,207 +350,422 @@ const showConditionTriggerDialog = () => {
|
||||
package_ids: 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 = () => {
|
||||
Object.assign(singleForm, {
|
||||
card_id: undefined,
|
||||
task_type: ''
|
||||
})
|
||||
singleDialogVisible.value = true
|
||||
loadCards() // 加载初始卡列表
|
||||
nextTick(() => {
|
||||
singleFormRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
const batchRules: FormRules = {
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!batchFormRef.value) return
|
||||
await batchFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const conditionRules: FormRules = {
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
limit: [{ required: true, message: '请输入限制数量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
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))
|
||||
const singleRules: FormRules = {
|
||||
card_id: [{ required: true, message: '请输入卡ID', trigger: 'blur' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
if (cardIds.length > 1000) {
|
||||
ElMessage.warning('卡ID数量不能超过1000个')
|
||||
return
|
||||
}
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
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 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 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 getStatusTag = (status: string) => {
|
||||
const typeMap: Record<string, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
pending: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
})
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const handleSingleSubmit = async () => {
|
||||
if (!singleFormRef.value) return
|
||||
await singleFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const getProgressPercentage = (row: ManualTriggerLog) => {
|
||||
if (row.total_count === 0) return 0
|
||||
return Math.round((row.processed_count / row.total_count) * 100)
|
||||
}
|
||||
|
||||
try {
|
||||
await ManualTriggerService.singleTrigger({
|
||||
card_id: singleForm.card_id!,
|
||||
task_type: singleForm.task_type as TaskType
|
||||
// 动态列配置
|
||||
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'
|
||||
})
|
||||
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()
|
||||
} catch (error) {
|
||||
ElMessage.error('单卡触发失败')
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('取消任务失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelTask = async (row: ManualTriggerLog) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const handleRowContextMenu = (row: ManualTriggerLog, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentClickRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
await ManualTriggerService.cancelTrigger({ trigger_id: row.id })
|
||||
ElMessage.success('任务已取消')
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载店铺树形数据
|
||||
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()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载店铺树形数据
|
||||
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()
|
||||
})
|
||||
loadShops()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.manual-trigger-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.manual-trigger-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -290,17 +290,17 @@
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
|
||||
&.total {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -327,9 +327,9 @@
|
||||
flex: 1;
|
||||
|
||||
.stat-label {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@@ -345,8 +345,8 @@
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
@@ -361,14 +361,14 @@
|
||||
|
||||
.info-item {
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
:value="role.ID"
|
||||
>
|
||||
<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>
|
||||
<ElTag type="success" size="small">客户角色</ElTag>
|
||||
@@ -273,7 +273,7 @@
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<div style="margin-top: 8px; color: #909399; font-size: 12px">
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #909399">
|
||||
只能选择一个客户角色,选择后立即生效
|
||||
</div>
|
||||
</div>
|
||||
@@ -1076,10 +1076,10 @@
|
||||
}
|
||||
|
||||
.current-role-display {
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 24px;
|
||||
border: 2px solid var(--el-color-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
&.no-role {
|
||||
border: 2px dashed var(--el-border-color);
|
||||
@@ -1090,30 +1090,30 @@
|
||||
}
|
||||
|
||||
.current-role-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.current-role-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.default-roles-section {
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -279,8 +279,8 @@
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -294,10 +294,10 @@
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.el-icon {
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
:close-on-click-modal="false"
|
||||
@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">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="配置名称" prop="name">
|
||||
@@ -103,10 +109,7 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="小程序AppID" prop="miniapp_app_id">
|
||||
<ElInput
|
||||
v-model="form.miniapp_app_id"
|
||||
placeholder="请输入小程序AppID"
|
||||
/>
|
||||
<ElInput v-model="form.miniapp_app_id" placeholder="请输入小程序AppID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -127,10 +130,7 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公众号AppID" prop="oa_app_id">
|
||||
<ElInput
|
||||
v-model="form.oa_app_id"
|
||||
placeholder="请输入公众号AppID"
|
||||
/>
|
||||
<ElInput v-model="form.oa_app_id" placeholder="请输入公众号AppID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -167,10 +167,7 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
|
||||
<ElInput
|
||||
v-model="form.oa_oauth_redirect_url"
|
||||
placeholder="请输入OAuth回调地址"
|
||||
/>
|
||||
<ElInput v-model="form.oa_oauth_redirect_url" placeholder="请输入OAuth回调地址" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">
|
||||
@@ -179,18 +176,12 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="微信商户号" prop="wx_mch_id">
|
||||
<ElInput
|
||||
v-model="form.wx_mch_id"
|
||||
placeholder="请输入微信商户号"
|
||||
/>
|
||||
<ElInput v-model="form.wx_mch_id" placeholder="请输入微信商户号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="证书序列号" prop="wx_serial_no">
|
||||
<ElInput
|
||||
v-model="form.wx_serial_no"
|
||||
placeholder="请输入微信证书序列号"
|
||||
/>
|
||||
<ElInput v-model="form.wx_serial_no" placeholder="请输入微信证书序列号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
@@ -217,10 +208,7 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="支付回调地址" prop="wx_notify_url">
|
||||
<ElInput
|
||||
v-model="form.wx_notify_url"
|
||||
placeholder="请输入微信支付回调地址"
|
||||
/>
|
||||
<ElInput v-model="form.wx_notify_url" placeholder="请输入微信支付回调地址" />
|
||||
</ElFormItem>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
@@ -252,10 +240,7 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="小程序AppID" prop="miniapp_app_id">
|
||||
<ElInput
|
||||
v-model="form.miniapp_app_id"
|
||||
placeholder="请输入小程序AppID"
|
||||
/>
|
||||
<ElInput v-model="form.miniapp_app_id" placeholder="请输入小程序AppID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -276,10 +261,7 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公众号AppID" prop="oa_app_id">
|
||||
<ElInput
|
||||
v-model="form.oa_app_id"
|
||||
placeholder="请输入公众号AppID"
|
||||
/>
|
||||
<ElInput v-model="form.oa_app_id" placeholder="请输入公众号AppID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -316,10 +298,7 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
|
||||
<ElInput
|
||||
v-model="form.oa_oauth_redirect_url"
|
||||
placeholder="请输入OAuth回调地址"
|
||||
/>
|
||||
<ElInput v-model="form.oa_oauth_redirect_url" placeholder="请输入OAuth回调地址" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">
|
||||
@@ -328,18 +307,12 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="微信商户号" prop="wx_mch_id">
|
||||
<ElInput
|
||||
v-model="form.wx_mch_id"
|
||||
placeholder="请输入微信商户号"
|
||||
/>
|
||||
<ElInput v-model="form.wx_mch_id" placeholder="请输入微信商户号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="证书序列号" prop="wx_serial_no">
|
||||
<ElInput
|
||||
v-model="form.wx_serial_no"
|
||||
placeholder="请输入微信证书序列号"
|
||||
/>
|
||||
<ElInput v-model="form.wx_serial_no" placeholder="请输入微信证书序列号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
@@ -366,10 +339,7 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="支付回调地址" prop="wx_notify_url">
|
||||
<ElInput
|
||||
v-model="form.wx_notify_url"
|
||||
placeholder="请输入微信支付回调地址"
|
||||
/>
|
||||
<ElInput v-model="form.wx_notify_url" placeholder="请输入微信支付回调地址" />
|
||||
</ElFormItem>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
@@ -400,44 +370,29 @@
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="富友API地址" prop="fy_api_url">
|
||||
<ElInput
|
||||
v-model="form.fy_api_url"
|
||||
placeholder="请输入富友API地址"
|
||||
/>
|
||||
<ElInput v-model="form.fy_api_url" placeholder="请输入富友API地址" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="富友机构号" prop="fy_ins_cd">
|
||||
<ElInput
|
||||
v-model="form.fy_ins_cd"
|
||||
placeholder="请输入富友机构号"
|
||||
/>
|
||||
<ElInput v-model="form.fy_ins_cd" placeholder="请输入富友机构号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="富友商户号" prop="fy_mchnt_cd">
|
||||
<ElInput
|
||||
v-model="form.fy_mchnt_cd"
|
||||
placeholder="请输入富友商户号"
|
||||
/>
|
||||
<ElInput v-model="form.fy_mchnt_cd" placeholder="请输入富友商户号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="富友终端号" prop="fy_term_id">
|
||||
<ElInput
|
||||
v-model="form.fy_term_id"
|
||||
placeholder="请输入富友终端号"
|
||||
/>
|
||||
<ElInput v-model="form.fy_term_id" placeholder="请输入富友终端号" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="支付回调地址" prop="fy_notify_url">
|
||||
<ElInput
|
||||
v-model="form.fy_notify_url"
|
||||
placeholder="请输入富友支付回调地址"
|
||||
/>
|
||||
<ElInput v-model="form.fy_notify_url" placeholder="请输入富友支付回调地址" />
|
||||
</ElFormItem>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
@@ -1117,8 +1072,8 @@
|
||||
|
||||
.config-form {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
overflow-y: auto;
|
||||
|
||||
.divider-title {
|
||||
font-size: 14px;
|
||||
@@ -1127,7 +1082,7 @@
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 24px 0 20px 0;
|
||||
margin: 24px 0 20px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
class="permission-tree"
|
||||
>
|
||||
<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>
|
||||
<ElTag :type="data.perm_type === 1 ? 'info' : 'success'" size="small">
|
||||
{{ data.perm_type === 1 ? '菜单' : '按钮' }}
|
||||
@@ -519,7 +519,7 @@
|
||||
}
|
||||
|
||||
// 检查所有顶层节点
|
||||
availablePermissions.value.forEach(node => checkNode(node))
|
||||
availablePermissions.value.forEach((node) => checkNode(node))
|
||||
|
||||
return Array.from(parentIdsToHalfCheck)
|
||||
}
|
||||
@@ -568,7 +568,8 @@
|
||||
label: node.perm_name,
|
||||
perm_type: node.perm_type,
|
||||
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 findInTree = (treeNodes: PermissionTreeNode[], targetId: number, parentId?: number): number | null => {
|
||||
const findInTree = (
|
||||
treeNodes: PermissionTreeNode[],
|
||||
targetId: number,
|
||||
parentId?: number
|
||||
): number | null => {
|
||||
for (const node of treeNodes) {
|
||||
if (node.id === targetId) {
|
||||
return parentId || null
|
||||
@@ -727,7 +732,7 @@
|
||||
// 为每个勾选的节点找到所有父节点
|
||||
checkedKeys.forEach((key: number) => {
|
||||
const parents = getParentNodeIds(key)
|
||||
parents.forEach(parentId => allIds.add(parentId))
|
||||
parents.forEach((parentId) => allIds.add(parentId))
|
||||
})
|
||||
|
||||
return Array.from(allIds)
|
||||
@@ -1079,8 +1084,8 @@
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.dialog-title {
|
||||
@@ -1090,8 +1095,8 @@
|
||||
|
||||
.role-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.role-name {
|
||||
font-size: 14px;
|
||||
@@ -1102,25 +1107,25 @@
|
||||
|
||||
.permission-tree-transfer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
min-height: 500px;
|
||||
padding: 20px 0;
|
||||
|
||||
.transfer-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: 340px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-width: 340px;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
@@ -1134,8 +1139,8 @@
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
.permission-tree {
|
||||
:deep(.el-tree-node) {
|
||||
@@ -1144,8 +1149,8 @@
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
padding-right: 8px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__label) {
|
||||
@@ -1162,16 +1167,16 @@
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.tree-node-label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
@@ -1186,9 +1191,9 @@
|
||||
.transfer-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.el-button {
|
||||
|
||||
Reference in New Issue
Block a user