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

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

View File

@@ -35,20 +35,24 @@
根据提供的权限JSON需要添加权限控制的模块包括
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>
```

View File

@@ -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. 测试与文档
- [ ] **权限控制测试**
- 创建不同权限组合的测试用户
- 验证每个页面的按钮显示/隐藏符合预期
- 验证状态切换的启用/禁用符合预期

View File

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

View File

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

View File

@@ -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 交互符合预期
- 边界情况处理正确

View File

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

View File

@@ -25,6 +25,7 @@ The system SHALL support filtering devices by package series ID in query paramet
The system SHALL include package series ID in device response data.
**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,

View File

@@ -25,6 +25,7 @@ The system SHALL support filtering standalone IoT cards by package series ID in
The system SHALL include package series ID in standalone IoT card response data.
**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,

View File

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

View File

@@ -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
- 单套餐分配页面 - 需展示新增的关联字段

View File

@@ -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创建套餐系列并保存梯度佣金配置

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
)
}
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,在其基础上添加空值处理

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('下载失败数据失败')
}
}

View File

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

View File

@@ -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: '新资产标识符',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('回收操作失败,请重试')
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: '重新提交'

View File

@@ -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 || '&#45;&#45;'-->
<!-- }}</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"-->
<!--&gt;-->
<!-- 设置限速-->
<!--</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 || '&#45;&#45;'-->
<!--}}</ElDescriptionsItem>-->
<ElDescriptionsItem label="套餐系列">{{
cardInfo?.series_name || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="批次号">{{
cardInfo?.batch_no || '--'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">{{
formatDateTime(cardInfo?.created_at)
}}</ElDescriptionsItem>
<!--<template v-if="cardInfo?.asset_type === 'card'">-->
<!-- <ElDescriptionsItem label="供应商">{{-->
<!-- cardInfo?.supplier || '&#45;&#45;'-->
<!-- }}</ElDescriptionsItem>-->
<!--</template>-->
</ElDescriptions>
</ElCard>
</div>
<!-- 右侧钱包概况 -->
<div class="col">
<!-- 第三行钱包概况和套餐列表 -->
<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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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