This commit is contained in:
77
src/api/modules/alert.ts
Normal file
77
src/api/modules/alert.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 轮询告警管理 API
|
||||
*/
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
PollingAlertRule,
|
||||
PollingAlertHistory,
|
||||
CreatePollingAlertRuleRequest,
|
||||
UpdatePollingAlertRuleRequest,
|
||||
PollingAlertHistoryQueryParams
|
||||
} from '@/types/api/alert'
|
||||
|
||||
export class PollingAlertService extends BaseService {
|
||||
/**
|
||||
* 获取告警规则列表
|
||||
*/
|
||||
static getPollingAlertRules(): Promise<BaseResponse<{ items: PollingAlertRule[] }>> {
|
||||
return this.get<BaseResponse<{ items: PollingAlertRule[] }>>('/api/admin/polling-alert-rules')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取告警规则详情
|
||||
*/
|
||||
static getPollingAlertRuleById(id: number): Promise<BaseResponse<PollingAlertRule>> {
|
||||
return this.get<BaseResponse<PollingAlertRule>>(`/api/admin/polling-alert-rules/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建告警规则
|
||||
*/
|
||||
static createPollingAlertRule(
|
||||
data: CreatePollingAlertRuleRequest
|
||||
): Promise<BaseResponse<PollingAlertRule>> {
|
||||
return this.post<BaseResponse<PollingAlertRule>>('/api/admin/polling-alert-rules', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新告警规则
|
||||
*/
|
||||
static updatePollingAlertRule(
|
||||
id: number,
|
||||
data: UpdatePollingAlertRuleRequest
|
||||
): Promise<BaseResponse<null>> {
|
||||
return this.put<BaseResponse<null>>(`/api/admin/polling-alert-rules/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除告警规则
|
||||
*/
|
||||
static deletePollingAlertRule(id: number): Promise<BaseResponse<null>> {
|
||||
return this.delete<BaseResponse<null>>(`/api/admin/polling-alert-rules/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取告警历史列表
|
||||
*/
|
||||
static getPollingAlertHistory(params?: PollingAlertHistoryQueryParams): Promise<
|
||||
BaseResponse<{
|
||||
items: PollingAlertHistory[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
total_pages: number
|
||||
}>
|
||||
> {
|
||||
return this.get<
|
||||
BaseResponse<{
|
||||
items: PollingAlertHistory[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
total_pages: number
|
||||
}>
|
||||
>('/api/admin/polling-alert-history', params)
|
||||
}
|
||||
}
|
||||
103
src/api/modules/dataCleanup.ts
Normal file
103
src/api/modules/dataCleanup.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 数据清理管理 API
|
||||
*/
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
DataCleanupConfig,
|
||||
CreateDataCleanupConfigRequest,
|
||||
UpdateDataCleanupConfigRequest,
|
||||
DataCleanupLog,
|
||||
DataCleanupLogQueryParams,
|
||||
DataCleanupPreviewItem,
|
||||
DataCleanupProgress,
|
||||
TriggerDataCleanupRequest
|
||||
} from '@/types/api/dataCleanup'
|
||||
|
||||
export class DataCleanupService extends BaseService {
|
||||
/**
|
||||
* 获取数据清理配置列表
|
||||
*/
|
||||
static getDataCleanupConfigs(): Promise<BaseResponse<{ items: DataCleanupConfig[] }>> {
|
||||
return this.get<BaseResponse<{ items: DataCleanupConfig[] }>>('/api/admin/data-cleanup-configs')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据清理配置详情
|
||||
*/
|
||||
static getDataCleanupConfigById(id: number): Promise<BaseResponse<DataCleanupConfig>> {
|
||||
return this.get<BaseResponse<DataCleanupConfig>>(`/api/admin/data-cleanup-configs/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据清理配置
|
||||
*/
|
||||
static createDataCleanupConfig(
|
||||
data: CreateDataCleanupConfigRequest
|
||||
): Promise<BaseResponse<DataCleanupConfig>> {
|
||||
return this.post<BaseResponse<DataCleanupConfig>>('/api/admin/data-cleanup-configs', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据清理配置
|
||||
*/
|
||||
static updateDataCleanupConfig(
|
||||
id: number,
|
||||
data: UpdateDataCleanupConfigRequest
|
||||
): Promise<BaseResponse<null>> {
|
||||
return this.put<BaseResponse<null>>(`/api/admin/data-cleanup-configs/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据清理配置
|
||||
*/
|
||||
static deleteDataCleanupConfig(id: number): Promise<BaseResponse<null>> {
|
||||
return this.delete<BaseResponse<null>>(`/api/admin/data-cleanup-configs/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据清理日志列表
|
||||
*/
|
||||
static getDataCleanupLogs(params?: DataCleanupLogQueryParams): Promise<
|
||||
BaseResponse<{
|
||||
items: DataCleanupLog[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
total_pages: number
|
||||
}>
|
||||
> {
|
||||
return this.get<
|
||||
BaseResponse<{
|
||||
items: DataCleanupLog[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
total_pages: number
|
||||
}>
|
||||
>('/api/admin/data-cleanup-logs', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览待清理数据
|
||||
*/
|
||||
static previewDataCleanup(): Promise<BaseResponse<{ items: DataCleanupPreviewItem[] }>> {
|
||||
return this.get<BaseResponse<{ items: DataCleanupPreviewItem[] }>>(
|
||||
'/api/admin/data-cleanup/preview'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据清理进度
|
||||
*/
|
||||
static getDataCleanupProgress(): Promise<BaseResponse<DataCleanupProgress>> {
|
||||
return this.get<BaseResponse<DataCleanupProgress>>('/api/admin/data-cleanup/progress')
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发数据清理
|
||||
*/
|
||||
static triggerDataCleanup(data?: TriggerDataCleanupRequest): Promise<BaseResponse<null>> {
|
||||
return this.post<BaseResponse<null>>('/api/admin/data-cleanup/trigger', data || {})
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,13 @@ export { AssetService } from './asset'
|
||||
export { AgentRechargeService } from './agentRecharge'
|
||||
export { WechatConfigService } from './wechatConfig'
|
||||
export { ExchangeService } from './exchange'
|
||||
export { RefundService } from './refund'
|
||||
export { DataCleanupService } from './dataCleanup'
|
||||
export { PollingAlertService } from './alert'
|
||||
export { PollingConcurrencyService } from './pollingConcurrency'
|
||||
export { PollingConfigService } from './pollingConfig'
|
||||
export { ManualTriggerService } from './manualTrigger'
|
||||
export { PollingMonitorService } from './pollingMonitor'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
88
src/api/modules/manualTrigger.ts
Normal file
88
src/api/modules/manualTrigger.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 手动触发相关 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
ManualTriggerLog,
|
||||
BatchManualTriggerRequest,
|
||||
ConditionManualTriggerRequest,
|
||||
SingleManualTriggerRequest,
|
||||
CancelManualTriggerRequest,
|
||||
ManualTriggerHistoryQueryParams,
|
||||
ManualTriggerHistoryResponse,
|
||||
ManualTriggerStatusResponse,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class ManualTriggerService extends BaseService {
|
||||
/**
|
||||
* 批量手动触发
|
||||
* POST /api/admin/polling-manual-trigger/batch
|
||||
*/
|
||||
static batchTrigger(
|
||||
data: BatchManualTriggerRequest
|
||||
): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
return this.post<BaseResponse<ManualTriggerLog>>(
|
||||
'/api/admin/polling-manual-trigger/batch',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件筛选触发
|
||||
* POST /api/admin/polling-manual-trigger/by-condition
|
||||
*/
|
||||
static conditionTrigger(
|
||||
data: ConditionManualTriggerRequest
|
||||
): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
return this.post<BaseResponse<ManualTriggerLog>>(
|
||||
'/api/admin/polling-manual-trigger/by-condition',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 单卡手动触发
|
||||
* POST /api/admin/polling-manual-trigger/single
|
||||
*/
|
||||
static singleTrigger(
|
||||
data: SingleManualTriggerRequest
|
||||
): Promise<BaseResponse<ManualTriggerLog>> {
|
||||
return this.post<BaseResponse<ManualTriggerLog>>(
|
||||
'/api/admin/polling-manual-trigger/single',
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消手动触发任务
|
||||
* POST /api/admin/polling-manual-trigger/cancel
|
||||
*/
|
||||
static cancelTrigger(data: CancelManualTriggerRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>('/api/admin/polling-manual-trigger/cancel', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取手动触发历史
|
||||
* GET /api/admin/polling-manual-trigger/history
|
||||
*/
|
||||
static getHistory(
|
||||
params?: ManualTriggerHistoryQueryParams
|
||||
): Promise<BaseResponse<ManualTriggerHistoryResponse>> {
|
||||
return this.get<BaseResponse<ManualTriggerHistoryResponse>>(
|
||||
'/api/admin/polling-manual-trigger/history',
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取手动触发状态
|
||||
* GET /api/admin/polling-manual-trigger/status
|
||||
*/
|
||||
static getStatus(): Promise<BaseResponse<ManualTriggerStatusResponse>> {
|
||||
return this.get<BaseResponse<ManualTriggerStatusResponse>>(
|
||||
'/api/admin/polling-manual-trigger/status'
|
||||
)
|
||||
}
|
||||
}
|
||||
50
src/api/modules/pollingConcurrency.ts
Normal file
50
src/api/modules/pollingConcurrency.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 轮询并发控制 API
|
||||
*/
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
PollingConcurrency,
|
||||
PollingConcurrencyListResponse,
|
||||
UpdateConcurrencyParams,
|
||||
ResetConcurrencyParams
|
||||
} from '@/types/api/pollingConcurrency'
|
||||
|
||||
/**
|
||||
* 轮询并发控制服务
|
||||
*/
|
||||
export class PollingConcurrencyService extends BaseService {
|
||||
/**
|
||||
* 获取轮询并发配置列表
|
||||
* @returns 并发配置列表
|
||||
*/
|
||||
static getConcurrencyList(): Promise<BaseResponse<PollingConcurrencyListResponse>> {
|
||||
return this.get<BaseResponse<PollingConcurrencyListResponse>>('/api/admin/polling-concurrency')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定任务类型的并发配置
|
||||
* @param taskType 任务类型
|
||||
* @returns 并发配置
|
||||
*/
|
||||
static getConcurrencyByType(taskType: string): Promise<BaseResponse<PollingConcurrency>> {
|
||||
return this.get<BaseResponse<PollingConcurrency>>(`/api/admin/polling-concurrency/${taskType}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮询并发配置
|
||||
* @param taskType 任务类型
|
||||
* @param params 更新参数
|
||||
*/
|
||||
static updateConcurrency(taskType: string, params: UpdateConcurrencyParams): Promise<BaseResponse<void>> {
|
||||
return this.put<BaseResponse<void>>(`/api/admin/polling-concurrency/${taskType}`, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置轮询并发计数
|
||||
* @param params 重置参数
|
||||
*/
|
||||
static resetConcurrency(params?: ResetConcurrencyParams): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>('/api/admin/polling-concurrency/reset', params || {})
|
||||
}
|
||||
}
|
||||
90
src/api/modules/pollingConfig.ts
Normal file
90
src/api/modules/pollingConfig.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 轮询配置管理 API
|
||||
*/
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
PollingConfig,
|
||||
PollingConfigQueryParams,
|
||||
PollingConfigListResponse,
|
||||
CreatePollingConfigRequest,
|
||||
UpdatePollingConfigRequest,
|
||||
UpdatePollingConfigStatusRequest
|
||||
} from '@/types/api/pollingConfig'
|
||||
|
||||
/**
|
||||
* 轮询配置管理服务
|
||||
*/
|
||||
export class PollingConfigService extends BaseService {
|
||||
/**
|
||||
* 获取轮询配置列表
|
||||
* @param params 查询参数
|
||||
* @returns 配置列表
|
||||
*/
|
||||
static getPollingConfigs(
|
||||
params?: PollingConfigQueryParams
|
||||
): Promise<BaseResponse<PollingConfigListResponse>> {
|
||||
return this.get<BaseResponse<PollingConfigListResponse>>('/api/admin/polling-configs', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取轮询配置详情
|
||||
* @param id 配置ID
|
||||
* @returns 配置详情
|
||||
*/
|
||||
static getPollingConfigById(id: number): Promise<BaseResponse<PollingConfig>> {
|
||||
return this.get<BaseResponse<PollingConfig>>(`/api/admin/polling-configs/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建轮询配置
|
||||
* @param data 创建参数
|
||||
* @returns 创建的配置
|
||||
*/
|
||||
static createPollingConfig(
|
||||
data: CreatePollingConfigRequest
|
||||
): Promise<BaseResponse<PollingConfig>> {
|
||||
return this.post<BaseResponse<PollingConfig>>('/api/admin/polling-configs', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮询配置
|
||||
* @param id 配置ID
|
||||
* @param data 更新参数
|
||||
* @returns 更新后的配置
|
||||
*/
|
||||
static updatePollingConfig(
|
||||
id: number,
|
||||
data: UpdatePollingConfigRequest
|
||||
): Promise<BaseResponse<PollingConfig>> {
|
||||
return this.put<BaseResponse<PollingConfig>>(`/api/admin/polling-configs/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除轮询配置
|
||||
* @param id 配置ID
|
||||
*/
|
||||
static deletePollingConfig(id: number): Promise<BaseResponse<void>> {
|
||||
return this.delete<BaseResponse<void>>(`/api/admin/polling-configs/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮询配置状态
|
||||
* @param id 配置ID
|
||||
* @param data 状态参数
|
||||
*/
|
||||
static updatePollingConfigStatus(
|
||||
id: number,
|
||||
data: UpdatePollingConfigStatusRequest
|
||||
): Promise<BaseResponse<void>> {
|
||||
return this.put<BaseResponse<void>>(`/api/admin/polling-configs/${id}/status`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的配置
|
||||
* @returns 启用的配置列表
|
||||
*/
|
||||
static getEnabledPollingConfigs(): Promise<BaseResponse<PollingConfig[]>> {
|
||||
return this.get<BaseResponse<PollingConfig[]>>('/api/admin/polling-configs/enabled')
|
||||
}
|
||||
}
|
||||
46
src/api/modules/pollingMonitor.ts
Normal file
46
src/api/modules/pollingMonitor.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 轮询监控相关 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
PollingOverviewStats,
|
||||
PollingInitProgress,
|
||||
PollingQueueStatusResponse,
|
||||
PollingTaskStatsResponse,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class PollingMonitorService extends BaseService {
|
||||
/**
|
||||
* 获取轮询总览统计
|
||||
* GET /api/admin/polling-stats
|
||||
*/
|
||||
static getOverviewStats(): Promise<BaseResponse<PollingOverviewStats>> {
|
||||
return this.get<BaseResponse<PollingOverviewStats>>('/api/admin/polling-stats')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取轮询初始化进度
|
||||
* GET /api/admin/polling-stats/init-progress
|
||||
*/
|
||||
static getInitProgress(): Promise<BaseResponse<PollingInitProgress>> {
|
||||
return this.get<BaseResponse<PollingInitProgress>>('/api/admin/polling-stats/init-progress')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取轮询队列状态
|
||||
* GET /api/admin/polling-stats/queues
|
||||
*/
|
||||
static getQueueStatus(): Promise<BaseResponse<PollingQueueStatusResponse>> {
|
||||
return this.get<BaseResponse<PollingQueueStatusResponse>>('/api/admin/polling-stats/queues')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取轮询任务统计
|
||||
* GET /api/admin/polling-stats/tasks
|
||||
*/
|
||||
static getTaskStats(): Promise<BaseResponse<PollingTaskStatsResponse>> {
|
||||
return this.get<BaseResponse<PollingTaskStatsResponse>>('/api/admin/polling-stats/tasks')
|
||||
}
|
||||
}
|
||||
78
src/api/modules/refund.ts
Normal file
78
src/api/modules/refund.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 退款管理相关 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type {
|
||||
Refund,
|
||||
RefundQueryParams,
|
||||
RefundListResponse,
|
||||
CreateRefundRequest,
|
||||
ApproveRefundRequest,
|
||||
RejectRefundRequest,
|
||||
ResubmitRefundRequest,
|
||||
ReturnRefundRequest,
|
||||
BaseResponse
|
||||
} from '@/types/api'
|
||||
|
||||
export class RefundService extends BaseService {
|
||||
/**
|
||||
* 获取退款申请列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
static getRefunds(params?: RefundQueryParams): Promise<BaseResponse<RefundListResponse>> {
|
||||
return this.get<BaseResponse<RefundListResponse>>('/api/admin/refunds', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取退款申请详情
|
||||
* @param id 退款申请ID
|
||||
*/
|
||||
static getRefundById(id: number): Promise<BaseResponse<Refund>> {
|
||||
return this.getOne<Refund>(`/api/admin/refunds/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建退款申请
|
||||
* @param data 创建退款申请请求参数
|
||||
*/
|
||||
static createRefund(data: CreateRefundRequest): Promise<BaseResponse<Refund>> {
|
||||
return this.post<BaseResponse<Refund>>('/api/admin/refunds', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过退款申请
|
||||
* @param id 退款申请ID
|
||||
* @param data 审批通过请求参数
|
||||
*/
|
||||
static approveRefund(id: number, data: ApproveRefundRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/approve`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批拒绝退款申请
|
||||
* @param id 退款申请ID
|
||||
* @param data 审批拒绝请求参数
|
||||
*/
|
||||
static rejectRefund(id: number, data: RejectRefundRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/reject`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新提交退款申请
|
||||
* @param id 退款申请ID
|
||||
* @param data 重新提交请求参数
|
||||
*/
|
||||
static resubmitRefund(id: number, data: ResubmitRefundRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/resubmit`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退回退款申请
|
||||
* @param id 退款申请ID
|
||||
* @param data 退回请求参数
|
||||
*/
|
||||
static returnRefund(id: number, data: ReturnRefundRequest): Promise<BaseResponse<void>> {
|
||||
return this.post<BaseResponse<void>>(`/api/admin/refunds/${id}/return`, data)
|
||||
}
|
||||
}
|
||||
@@ -1193,6 +1193,25 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'refund',
|
||||
name: 'RefundManagement',
|
||||
component: RoutesAlias.RefundManagement,
|
||||
meta: {
|
||||
title: '退款管理',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'refund/detail/:id',
|
||||
name: 'RefundDetailRoute',
|
||||
component: RoutesAlias.RefundDetail,
|
||||
meta: {
|
||||
title: '退款详情',
|
||||
isHide: true,
|
||||
keepAlive: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1305,5 +1324,81 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// }
|
||||
// }
|
||||
]
|
||||
},
|
||||
|
||||
// 轮询管理
|
||||
{
|
||||
path: '/polling-management',
|
||||
name: 'PollingManagement',
|
||||
component: RoutesAlias.Home,
|
||||
meta: {
|
||||
title: '轮询管理',
|
||||
icon: ''
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'data-cleanup',
|
||||
name: 'DataCleanup',
|
||||
component: RoutesAlias.DataCleanup,
|
||||
meta: {
|
||||
title: '数据清理',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'alert-rules',
|
||||
name: 'AlertRules',
|
||||
component: RoutesAlias.AlertRules,
|
||||
meta: {
|
||||
title: '告警规则',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'alert-history',
|
||||
name: 'AlertHistory',
|
||||
component: RoutesAlias.AlertHistory,
|
||||
meta: {
|
||||
title: '告警历史',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'concurrency',
|
||||
name: 'PollingConcurrency',
|
||||
component: RoutesAlias.PollingConcurrency,
|
||||
meta: {
|
||||
title: '并发控制',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'PollingConfig',
|
||||
component: RoutesAlias.PollingConfig,
|
||||
meta: {
|
||||
title: '轮询配置',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'manual-trigger',
|
||||
name: 'ManualTrigger',
|
||||
component: RoutesAlias.ManualTrigger,
|
||||
meta: {
|
||||
title: '手动触发',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'monitor',
|
||||
name: 'PollingMonitor',
|
||||
component: RoutesAlias.PollingMonitor,
|
||||
meta: {
|
||||
title: '轮询监控',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -129,7 +129,20 @@ export enum RoutesAlias {
|
||||
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
||||
WechatConfig = '/settings/wechat-config', // 微信支付配置
|
||||
WechatConfigDetail = '/settings/wechat-config/detail' // 微信支付配置详情
|
||||
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
|
||||
|
||||
// 退款管理
|
||||
RefundManagement = '/finance/refund', // 退款管理
|
||||
RefundDetail = '/finance/refund/detail', // 退款详情
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
AlertRules = '/polling-management/alert-rules', // 告警规则
|
||||
AlertHistory = '/polling-management/alert-history', // 告警历史
|
||||
PollingConcurrency = '/polling-management/concurrency', // 并发控制
|
||||
PollingConfig = '/polling-management/config', // 轮询配置
|
||||
ManualTrigger = '/polling-management/manual-trigger', // 手动触发
|
||||
PollingMonitor = '/polling-management/monitor' // 轮询监控
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
|
||||
74
src/types/api/alert.ts
Normal file
74
src/types/api/alert.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 轮询告警管理相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 告警规则
|
||||
*/
|
||||
export interface PollingAlertRule {
|
||||
id: number
|
||||
rule_name: string
|
||||
alert_level: string
|
||||
metric_type: string
|
||||
metric_type_name: string
|
||||
task_type: string
|
||||
task_type_name: string
|
||||
operator: string
|
||||
threshold: number
|
||||
cooldown_minutes: number
|
||||
notify_channels: string
|
||||
status: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 告警历史
|
||||
*/
|
||||
export interface PollingAlertHistory {
|
||||
id: number
|
||||
rule_id: number
|
||||
rule_name: string
|
||||
alert_level: string
|
||||
metric_type: string
|
||||
task_type: string
|
||||
threshold: number
|
||||
current_value: number
|
||||
message: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建告警规则请求
|
||||
*/
|
||||
export interface CreatePollingAlertRuleRequest {
|
||||
rule_name: string
|
||||
alert_level: string
|
||||
metric_type: string
|
||||
task_type: string
|
||||
threshold: number
|
||||
operator?: string
|
||||
cooldown_minutes?: number
|
||||
notify_channels?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新告警规则请求
|
||||
*/
|
||||
export interface UpdatePollingAlertRuleRequest {
|
||||
rule_name?: string
|
||||
alert_level?: string
|
||||
threshold?: number
|
||||
cooldown_minutes?: number
|
||||
notify_channels?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 告警历史查询参数
|
||||
*/
|
||||
export interface PollingAlertHistoryQueryParams {
|
||||
rule_id?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
78
src/types/api/dataCleanup.ts
Normal file
78
src/types/api/dataCleanup.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 数据清理配置相关类型定义
|
||||
*/
|
||||
|
||||
// 数据清理配置
|
||||
export interface DataCleanupConfig {
|
||||
id: number
|
||||
table_name: string
|
||||
description: string
|
||||
retention_days: number
|
||||
batch_size: number
|
||||
enabled: number // 0=禁用,1=启用
|
||||
created_at: string
|
||||
updated_at: string
|
||||
updated_by: number
|
||||
}
|
||||
|
||||
// 创建数据清理配置请求
|
||||
export interface CreateDataCleanupConfigRequest {
|
||||
table_name: string
|
||||
retention_days: number
|
||||
batch_size?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 更新数据清理配置请求
|
||||
export interface UpdateDataCleanupConfigRequest {
|
||||
retention_days?: number
|
||||
batch_size?: number
|
||||
description?: string
|
||||
enabled?: number
|
||||
}
|
||||
|
||||
// 数据清理日志
|
||||
export interface DataCleanupLog {
|
||||
id: number
|
||||
table_name: string
|
||||
cleanup_type: 'scheduled' | 'manual'
|
||||
status: 'success' | 'failed' | 'running'
|
||||
retention_days: number
|
||||
deleted_count: number
|
||||
duration_ms: number
|
||||
started_at: string
|
||||
completed_at: string | null
|
||||
error_message: string
|
||||
triggered_by: number | null
|
||||
}
|
||||
|
||||
// 数据清理日志查询参数
|
||||
export interface DataCleanupLogQueryParams {
|
||||
table_name?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
// 数据清理预览项
|
||||
export interface DataCleanupPreviewItem {
|
||||
table_name: string
|
||||
description: string
|
||||
retention_days: number
|
||||
record_count: number
|
||||
}
|
||||
|
||||
// 数据清理进度
|
||||
export interface DataCleanupProgress {
|
||||
is_running: boolean
|
||||
current_table: string
|
||||
total_tables: number
|
||||
processed_tables: number
|
||||
total_deleted: number
|
||||
started_at: string | null
|
||||
last_log: DataCleanupLog | null
|
||||
}
|
||||
|
||||
// 手动触发数据清理请求
|
||||
export interface TriggerDataCleanupRequest {
|
||||
table_name?: string
|
||||
}
|
||||
@@ -85,3 +85,24 @@ export * from './agentRecharge'
|
||||
|
||||
// 微信支付配置相关
|
||||
export * from './wechatConfig'
|
||||
|
||||
// 退款管理相关
|
||||
export * from './refund'
|
||||
|
||||
// 数据清理相关
|
||||
export * from './dataCleanup'
|
||||
|
||||
// 轮询告警相关
|
||||
export * from './alert'
|
||||
|
||||
// 轮询并发控制相关
|
||||
export * from './pollingConcurrency'
|
||||
|
||||
// 轮询配置管理相关
|
||||
export * from './pollingConfig'
|
||||
|
||||
// 手动触发相关
|
||||
export * from './manualTrigger'
|
||||
|
||||
// 轮询监控相关
|
||||
export * from './pollingMonitor'
|
||||
|
||||
140
src/types/api/manualTrigger.ts
Normal file
140
src/types/api/manualTrigger.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 手动触发相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 任务类型
|
||||
*/
|
||||
export type TaskType = 'polling:realname' | 'polling:carddata' | 'polling:package'
|
||||
|
||||
/**
|
||||
* 触发类型
|
||||
*/
|
||||
export type TriggerType = 'single' | 'batch' | 'by_condition'
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
*/
|
||||
export type ManualTriggerStatus = 'pending' | 'processing' | 'completed' | 'cancelled'
|
||||
|
||||
/**
|
||||
* 手动触发日志对象
|
||||
*/
|
||||
export interface ManualTriggerLog {
|
||||
/** 日志ID */
|
||||
id: number
|
||||
/** 任务类型 */
|
||||
task_type: TaskType
|
||||
/** 任务类型名称 */
|
||||
task_type_name: string
|
||||
/** 触发类型 */
|
||||
trigger_type: TriggerType
|
||||
/** 触发类型名称 */
|
||||
trigger_type_name: string
|
||||
/** 总卡数 */
|
||||
total_count: number
|
||||
/** 已处理数 */
|
||||
processed_count: number
|
||||
/** 成功数 */
|
||||
success_count: number
|
||||
/** 失败数 */
|
||||
failed_count: number
|
||||
/** 状态 */
|
||||
status: ManualTriggerStatus
|
||||
/** 状态名称 */
|
||||
status_name: string
|
||||
/** 触发时间 */
|
||||
triggered_at: string
|
||||
/** 完成时间 */
|
||||
completed_at: string | null
|
||||
/** 触发人ID */
|
||||
triggered_by: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量手动触发请求参数
|
||||
*/
|
||||
export interface BatchManualTriggerRequest {
|
||||
/** 任务类型 */
|
||||
task_type: TaskType
|
||||
/** 卡ID列表(最多1000个) */
|
||||
card_ids?: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件筛选触发请求参数
|
||||
*/
|
||||
export interface ConditionManualTriggerRequest {
|
||||
/** 任务类型 */
|
||||
task_type: TaskType
|
||||
/** 限制数量(最多1000) */
|
||||
limit: number
|
||||
/** 卡状态筛选 */
|
||||
card_status?: string
|
||||
/** 卡类型筛选 */
|
||||
card_type?: string
|
||||
/** 运营商代码筛选 */
|
||||
carrier_code?: string
|
||||
/** 是否启用轮询筛选 */
|
||||
enable_polling?: boolean
|
||||
/** 套餐ID列表筛选 */
|
||||
package_ids?: number[]
|
||||
/** 店铺ID筛选 */
|
||||
shop_id?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 单卡手动触发请求参数
|
||||
*/
|
||||
export interface SingleManualTriggerRequest {
|
||||
/** 卡ID */
|
||||
card_id: number
|
||||
/** 任务类型 */
|
||||
task_type: TaskType
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消手动触发请求参数
|
||||
*/
|
||||
export interface CancelManualTriggerRequest {
|
||||
/** 触发任务ID */
|
||||
trigger_id: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发历史查询参数
|
||||
*/
|
||||
export interface ManualTriggerHistoryQueryParams {
|
||||
/** 任务类型筛选 */
|
||||
task_type?: TaskType
|
||||
/** 页码 */
|
||||
page?: number
|
||||
/** 每页数量 */
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发历史列表响应
|
||||
*/
|
||||
export interface ManualTriggerHistoryResponse {
|
||||
/** 日志列表 */
|
||||
items: ManualTriggerLog[]
|
||||
/** 当前页 */
|
||||
page: number
|
||||
/** 每页数量 */
|
||||
page_size: number
|
||||
/** 总数 */
|
||||
total: number
|
||||
/** 总页数 */
|
||||
total_pages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发状态响应
|
||||
*/
|
||||
export interface ManualTriggerStatusResponse {
|
||||
/** 各队列大小 */
|
||||
queue_sizes: Record<string, number>
|
||||
/** 正在运行的任务列表 */
|
||||
running_tasks: ManualTriggerLog[]
|
||||
}
|
||||
45
src/types/api/pollingConcurrency.ts
Normal file
45
src/types/api/pollingConcurrency.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 轮询并发控制相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 并发配置对象
|
||||
*/
|
||||
export interface PollingConcurrency {
|
||||
/** 任务类型 */
|
||||
task_type: string
|
||||
/** 任务类型名称 */
|
||||
task_type_name: string
|
||||
/** 最大并发数 */
|
||||
max_concurrency: number
|
||||
/** 当前并发数 */
|
||||
current: number
|
||||
/** 可用并发数 */
|
||||
available: number
|
||||
/** 使用率(百分比) */
|
||||
utilization: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发配置列表响应
|
||||
*/
|
||||
export interface PollingConcurrencyListResponse {
|
||||
/** 并发配置列表 */
|
||||
items: PollingConcurrency[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新并发配置请求参数
|
||||
*/
|
||||
export interface UpdateConcurrencyParams {
|
||||
/** 最大并发数(1-1000) */
|
||||
max_concurrency: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置并发计数请求参数
|
||||
*/
|
||||
export interface ResetConcurrencyParams {
|
||||
/** 任务类型(不传则重置全部) */
|
||||
task_type?: string
|
||||
}
|
||||
137
src/types/api/pollingConfig.ts
Normal file
137
src/types/api/pollingConfig.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 轮询配置管理相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 卡业务类型
|
||||
*/
|
||||
export type CardCategory = 'normal' | 'industry'
|
||||
|
||||
/**
|
||||
* 卡状态条件
|
||||
*/
|
||||
export type CardCondition = 'not_real_name' | 'real_name' | 'activated' | 'suspended'
|
||||
|
||||
/**
|
||||
* 轮询配置对象
|
||||
*/
|
||||
export interface PollingConfig {
|
||||
/** 配置ID */
|
||||
id: number
|
||||
/** 配置名称 */
|
||||
config_name: string
|
||||
/** 配置说明 */
|
||||
description: string
|
||||
/** 卡业务类型 */
|
||||
card_category: CardCategory
|
||||
/** 卡状态条件 */
|
||||
card_condition: CardCondition
|
||||
/** 运营商ID */
|
||||
carrier_id: number
|
||||
/** 优先级(数字越小优先级越高) */
|
||||
priority: number
|
||||
/** 状态(1:启用,0:禁用) */
|
||||
status: number
|
||||
/** 实名检查间隔(秒,NULL表示不检查) */
|
||||
realname_check_interval: number | null
|
||||
/** 套餐检查间隔(秒,NULL表示不检查) */
|
||||
package_check_interval: number | null
|
||||
/** 流量检查间隔(秒,NULL表示不检查) */
|
||||
carddata_check_interval: number | null
|
||||
/** 创建时间 */
|
||||
created_at: string
|
||||
/** 更新时间 */
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询配置查询参数
|
||||
*/
|
||||
export interface PollingConfigQueryParams {
|
||||
/** 页码(默认1) */
|
||||
page?: number
|
||||
/** 每页数量(1-100) */
|
||||
page_size?: number
|
||||
/** 状态(1:启用,0:禁用) */
|
||||
status?: number
|
||||
/** 卡状态条件 */
|
||||
card_condition?: CardCondition
|
||||
/** 卡业务类型 */
|
||||
card_category?: CardCategory
|
||||
/** 运营商ID */
|
||||
carrier_id?: number
|
||||
/** 配置名称(模糊搜索) */
|
||||
config_name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询配置列表响应
|
||||
*/
|
||||
export interface PollingConfigListResponse {
|
||||
/** 配置列表 */
|
||||
items: PollingConfig[]
|
||||
/** 当前页 */
|
||||
page: number
|
||||
/** 每页数量 */
|
||||
page_size: number
|
||||
/** 总数 */
|
||||
total: number
|
||||
/** 总页数 */
|
||||
total_pages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建轮询配置请求参数
|
||||
*/
|
||||
export interface CreatePollingConfigRequest {
|
||||
/** 配置名称(1-100字符) */
|
||||
config_name: string
|
||||
/** 优先级(1-1000,数字越小优先级越高) */
|
||||
priority: number
|
||||
/** 配置说明(最多500字符) */
|
||||
description?: string
|
||||
/** 卡业务类型 */
|
||||
card_category?: CardCategory
|
||||
/** 卡状态条件 */
|
||||
card_condition?: CardCondition
|
||||
/** 运营商ID */
|
||||
carrier_id?: number
|
||||
/** 实名检查间隔(秒,最小30,NULL表示不检查) */
|
||||
realname_check_interval?: number | null
|
||||
/** 套餐检查间隔(秒,最小60,NULL表示不检查) */
|
||||
package_check_interval?: number | null
|
||||
/** 流量检查间隔(秒,最小60,NULL表示不检查) */
|
||||
carddata_check_interval?: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮询配置请求参数
|
||||
*/
|
||||
export interface UpdatePollingConfigRequest {
|
||||
/** 配置名称(1-100字符) */
|
||||
config_name?: string
|
||||
/** 优先级(1-1000,数字越小优先级越高) */
|
||||
priority?: number
|
||||
/** 配置说明(最多500字符) */
|
||||
description?: string
|
||||
/** 卡业务类型 */
|
||||
card_category?: CardCategory
|
||||
/** 卡状态条件 */
|
||||
card_condition?: CardCondition
|
||||
/** 运营商ID */
|
||||
carrier_id?: number
|
||||
/** 实名检查间隔(秒,最小30,NULL表示不检查) */
|
||||
realname_check_interval?: number | null
|
||||
/** 套餐检查间隔(秒,最小60,NULL表示不检查) */
|
||||
package_check_interval?: number | null
|
||||
/** 流量检查间隔(秒,最小60,NULL表示不检查) */
|
||||
carddata_check_interval?: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮询配置状态请求参数
|
||||
*/
|
||||
export interface UpdatePollingConfigStatusRequest {
|
||||
/** 状态(1:启用,0:禁用) */
|
||||
status: number
|
||||
}
|
||||
95
src/types/api/pollingMonitor.ts
Normal file
95
src/types/api/pollingMonitor.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 轮询监控相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 轮询总览统计对象
|
||||
*/
|
||||
export interface PollingOverviewStats {
|
||||
/** 总卡数 */
|
||||
total_cards: number
|
||||
/** 已初始化卡数 */
|
||||
initialized_cards: number
|
||||
/** 是否正在初始化 */
|
||||
is_initializing: boolean
|
||||
/** 初始化进度(0-100) */
|
||||
init_progress: number
|
||||
/** 实名检查队列大小 */
|
||||
realname_queue_size: number
|
||||
/** 套餐检查队列大小 */
|
||||
package_queue_size: number
|
||||
/** 流量检查队列大小 */
|
||||
carddata_queue_size: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询初始化进度对象
|
||||
*/
|
||||
export interface PollingInitProgress {
|
||||
/** 总卡数 */
|
||||
total_cards: number
|
||||
/** 已初始化卡数 */
|
||||
initialized_cards: number
|
||||
/** 进度百分比(0-100) */
|
||||
progress: number
|
||||
/** 是否完成 */
|
||||
is_complete: boolean
|
||||
/** 开始时间 */
|
||||
started_at: string
|
||||
/** 预计完成时间 */
|
||||
estimated_eta: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询队列状态对象
|
||||
*/
|
||||
export interface PollingQueueStatus {
|
||||
/** 任务类型 */
|
||||
task_type: string
|
||||
/** 任务类型名称 */
|
||||
task_type_name: string
|
||||
/** 队列大小 */
|
||||
queue_size: number
|
||||
/** 到期待处理数 */
|
||||
due_count: number
|
||||
/** 手动触发待处理数 */
|
||||
manual_pending: number
|
||||
/** 平均等待时间(秒) */
|
||||
avg_wait_time_s: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列状态列表响应
|
||||
*/
|
||||
export interface PollingQueueStatusResponse {
|
||||
/** 队列状态列表 */
|
||||
items: PollingQueueStatus[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询任务统计对象
|
||||
*/
|
||||
export interface PollingTaskStats {
|
||||
/** 任务类型 */
|
||||
task_type: string
|
||||
/** 任务类型名称 */
|
||||
task_type_name: string
|
||||
/** 1小时总数 */
|
||||
total_count_1h: number
|
||||
/** 1小时成功数 */
|
||||
success_count_1h: number
|
||||
/** 1小时失败数 */
|
||||
failure_count_1h: number
|
||||
/** 成功率(0-100) */
|
||||
success_rate: number
|
||||
/** 平均耗时(毫秒) */
|
||||
avg_duration_ms: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务统计列表响应
|
||||
*/
|
||||
export interface PollingTaskStatsResponse {
|
||||
/** 任务统计列表 */
|
||||
items: PollingTaskStats[]
|
||||
}
|
||||
85
src/types/api/refund.ts
Normal file
85
src/types/api/refund.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 退款管理相关类型定义
|
||||
*/
|
||||
|
||||
// 退款状态
|
||||
export enum RefundStatus {
|
||||
PENDING = 1, // 待审批
|
||||
APPROVED = 2, // 已通过
|
||||
REJECTED = 3, // 已拒绝
|
||||
RETURNED = 4 // 已退回
|
||||
}
|
||||
|
||||
// 退款申请
|
||||
export interface Refund {
|
||||
id: number
|
||||
refund_no: string
|
||||
order_id: number
|
||||
shop_id: number
|
||||
package_usage_id: number | null
|
||||
requested_refund_amount: number // 申请退款金额(分)
|
||||
approved_refund_amount: number | null // 实际退款金额(分)
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
status: RefundStatus
|
||||
refund_reason: string
|
||||
reject_reason: string
|
||||
remark: string
|
||||
asset_reset: boolean
|
||||
commission_deducted: boolean
|
||||
creator: number
|
||||
processor_id: number | null
|
||||
processed_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
updater: number
|
||||
}
|
||||
|
||||
// 查询退款申请列表参数
|
||||
export interface RefundQueryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: RefundStatus
|
||||
order_id?: number
|
||||
shop_id?: number
|
||||
dateRange?: string[]
|
||||
}
|
||||
|
||||
// 退款申请列表响应
|
||||
export interface RefundListResponse {
|
||||
items: Refund[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// 创建退款申请请求
|
||||
export interface CreateRefundRequest {
|
||||
order_id: number
|
||||
package_usage_id?: number
|
||||
requested_refund_amount: number // 申请退款金额(分)
|
||||
actual_received_amount: number // 实收金额(分)
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
// 审批通过退款申请请求
|
||||
export interface ApproveRefundRequest {
|
||||
approved_refund_amount?: number // 实际退款金额(分)
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 审批拒绝退款申请请求
|
||||
export interface RejectRefundRequest {
|
||||
reject_reason: string
|
||||
}
|
||||
|
||||
// 重新提交退款申请请求
|
||||
export interface ResubmitRefundRequest {
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
refund_reason?: string
|
||||
}
|
||||
|
||||
// 退回退款申请请求
|
||||
export interface ReturnRefundRequest {
|
||||
remark?: string
|
||||
}
|
||||
565
src/views/finance/refund/detail.vue
Normal file
565
src/views/finance/refund/detail.vue
Normal file
@@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="refund-detail-page">
|
||||
<ElCard shadow="never" v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>退款详情</span>
|
||||
<ElButton @click="handleGoBack">返回</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElDescriptions :column="2" border v-if="refund">
|
||||
<ElDescriptionsItem label="退款单号">{{ refund.refund_no }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="getStatusType(refund.status)">{{ getStatusText(refund.status) }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="订单ID">{{ refund.order_id }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="店铺ID">{{ refund.shop_id }}</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="套餐使用记录ID">
|
||||
{{ refund.package_usage_id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请退款金额">
|
||||
{{ formatCurrency(refund.requested_refund_amount) }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="实际退款金额">
|
||||
{{
|
||||
refund.approved_refund_amount ? formatCurrency(refund.approved_refund_amount) : '-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="实收金额">
|
||||
{{ formatCurrency(refund.actual_received_amount) }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="资产重置">
|
||||
<ElTag :type="refund.asset_reset ? 'success' : 'info'">
|
||||
{{ refund.asset_reset ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="佣金扣除">
|
||||
<ElTag :type="refund.commission_deducted ? 'success' : 'info'">
|
||||
{{ refund.commission_deducted ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="退款原因" :span="2">
|
||||
{{ refund.refund_reason || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="拒绝原因" :span="2">
|
||||
{{ refund.reject_reason || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="审批备注" :span="2">
|
||||
{{ refund.remark || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="创建人ID">{{ refund.creator }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审批人ID">
|
||||
{{ refund.processor_id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="创建时间">
|
||||
{{ formatDateTime(refund.created_at) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审批时间">
|
||||
{{ refund.processed_at ? formatDateTime(refund.processed_at) : '-' }}
|
||||
</ElDescriptionsItem>
|
||||
|
||||
<ElDescriptionsItem label="更新时间">
|
||||
{{ formatDateTime(refund.updated_at) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新人ID">{{ refund.updater }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div class="action-buttons" v-if="refund">
|
||||
<!-- 待审批状态显示审批操作 -->
|
||||
<template v-if="refund.status === 1">
|
||||
<ElButton type="primary" @click="handleShowApprove">审批通过</ElButton>
|
||||
<ElButton type="danger" @click="handleShowReject">审批拒绝</ElButton>
|
||||
<ElButton type="warning" @click="handleShowReturn">退回</ElButton>
|
||||
</template>
|
||||
<!-- 已拒绝或已退回状态显示重新提交 -->
|
||||
<template v-if="refund.status === 3 || refund.status === 4">
|
||||
<ElButton type="primary" @click="handleShowResubmit">重新提交</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 审批通过对话框 -->
|
||||
<ElDialog
|
||||
v-model="approveDialogVisible"
|
||||
title="审批通过"
|
||||
width="500px"
|
||||
@closed="handleApproveDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="approveFormRef"
|
||||
:model="approveForm"
|
||||
:rules="approveRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="申请退款金额">
|
||||
<span>{{ formatCurrency(refund?.requested_refund_amount || 0) }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实际退款金额" prop="approved_refund_amount">
|
||||
<ElInputNumber
|
||||
v-model="approveForm.approved_refund_amount"
|
||||
:min="1"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="不填则默认使用申请金额"
|
||||
/>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
不填则默认使用申请金额
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="审批备注">
|
||||
<ElInput
|
||||
v-model="approveForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入审批备注"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="approveDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleApproveRefund" :loading="approveLoading">
|
||||
确认通过
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 审批拒绝对话框 -->
|
||||
<ElDialog
|
||||
v-model="rejectDialogVisible"
|
||||
title="审批拒绝"
|
||||
width="500px"
|
||||
@closed="handleRejectDialogClosed"
|
||||
>
|
||||
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="拒绝原因" prop="reject_reason">
|
||||
<ElInput
|
||||
v-model="rejectForm.reject_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入拒绝原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="danger" @click="handleRejectRefund" :loading="rejectLoading">
|
||||
确认拒绝
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 退回对话框 -->
|
||||
<ElDialog
|
||||
v-model="returnDialogVisible"
|
||||
title="退回退款申请"
|
||||
width="500px"
|
||||
@closed="handleReturnDialogClosed"
|
||||
>
|
||||
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="120px">
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退回备注">
|
||||
<ElInput
|
||||
v-model="returnForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入退回备注"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="returnDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="warning" @click="handleReturnRefund" :loading="returnLoading">
|
||||
确认退回
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 重新提交对话框 -->
|
||||
<ElDialog
|
||||
v-model="resubmitDialogVisible"
|
||||
title="重新提交退款申请"
|
||||
width="500px"
|
||||
@closed="handleResubmitDialogClosed"
|
||||
>
|
||||
<ElForm
|
||||
ref="resubmitFormRef"
|
||||
:model="resubmitForm"
|
||||
:rules="resubmitRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<ElFormItem label="退款单号">
|
||||
<span>{{ refund?.refund_no }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="申请退款金额">
|
||||
<ElInputNumber
|
||||
v-model="resubmitForm.requested_refund_amount"
|
||||
:min="1"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="不填则使用原金额"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="实收金额">
|
||||
<ElInputNumber
|
||||
v-model="resubmitForm.actual_received_amount"
|
||||
:min="1"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="不填则使用原金额"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款原因">
|
||||
<ElInput
|
||||
v-model="resubmitForm.refund_reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入退款原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
|
||||
确认提交
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { RefundService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
Refund,
|
||||
RefundStatus,
|
||||
ApproveRefundRequest,
|
||||
RejectRefundRequest,
|
||||
ReturnRefundRequest,
|
||||
ResubmitRefundRequest
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'RefundDetail' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const approveLoading = ref(false)
|
||||
const rejectLoading = ref(false)
|
||||
const returnLoading = ref(false)
|
||||
const resubmitLoading = ref(false)
|
||||
const refund = ref<Refund | null>(null)
|
||||
|
||||
const approveDialogVisible = ref(false)
|
||||
const rejectDialogVisible = ref(false)
|
||||
const returnDialogVisible = ref(false)
|
||||
const resubmitDialogVisible = ref(false)
|
||||
|
||||
const approveFormRef = ref<FormInstance>()
|
||||
const rejectFormRef = ref<FormInstance>()
|
||||
const returnFormRef = ref<FormInstance>()
|
||||
const resubmitFormRef = ref<FormInstance>()
|
||||
|
||||
const approveRules = reactive<FormRules>({})
|
||||
|
||||
const rejectRules = reactive<FormRules>({
|
||||
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const returnRules = reactive<FormRules>({})
|
||||
|
||||
const resubmitRules = reactive<FormRules>({})
|
||||
|
||||
const approveForm = reactive<ApproveRefundRequest>({
|
||||
approved_refund_amount: undefined,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rejectForm = reactive<RejectRefundRequest>({
|
||||
reject_reason: ''
|
||||
})
|
||||
|
||||
const returnForm = reactive<ReturnRefundRequest>({
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const resubmitForm = reactive<{
|
||||
requested_refund_amount?: number
|
||||
actual_received_amount?: number
|
||||
refund_reason?: string
|
||||
}>({
|
||||
requested_refund_amount: undefined,
|
||||
actual_received_amount: undefined,
|
||||
refund_reason: ''
|
||||
})
|
||||
|
||||
// 格式化货币 - 将分转换为元
|
||||
const formatCurrency = (amount: number): string => {
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (
|
||||
status: RefundStatus
|
||||
): 'warning' | 'success' | 'danger' | 'info' => {
|
||||
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
1: 'warning', // 待审批
|
||||
2: 'success', // 已通过
|
||||
3: 'danger', // 已拒绝
|
||||
4: 'info' // 已退回
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: RefundStatus): string => {
|
||||
const statusMap: Record<RefundStatus, string> = {
|
||||
1: '待审批',
|
||||
2: '已通过',
|
||||
3: '已拒绝',
|
||||
4: '已退回'
|
||||
}
|
||||
return statusMap[status] || '-'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.params.id as string
|
||||
if (id) {
|
||||
fetchRefundDetail(Number(id))
|
||||
}
|
||||
})
|
||||
|
||||
// 获取退款详情
|
||||
const fetchRefundDetail = async (id: number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await RefundService.getRefundById(id)
|
||||
if (res.code === 0) {
|
||||
refund.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleGoBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 显示审批通过对话框
|
||||
const handleShowApprove = () => {
|
||||
approveDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 审批通过对话框关闭后的清理
|
||||
const handleApproveDialogClosed = () => {
|
||||
approveFormRef.value?.resetFields()
|
||||
approveForm.approved_refund_amount = undefined
|
||||
approveForm.remark = ''
|
||||
}
|
||||
|
||||
// 审批通过
|
||||
const handleApproveRefund = async () => {
|
||||
if (!approveFormRef.value || !refund.value) return
|
||||
|
||||
await approveFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
approveLoading.value = true
|
||||
try {
|
||||
const data: ApproveRefundRequest = {
|
||||
approved_refund_amount: approveForm.approved_refund_amount
|
||||
? approveForm.approved_refund_amount * 100
|
||||
: undefined,
|
||||
remark: approveForm.remark || undefined
|
||||
}
|
||||
|
||||
await RefundService.approveRefund(refund.value.id, data)
|
||||
ElMessage.success('审批通过成功')
|
||||
approveDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
approveLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示审批拒绝对话框
|
||||
const handleShowReject = () => {
|
||||
rejectDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 审批拒绝对话框关闭后的清理
|
||||
const handleRejectDialogClosed = () => {
|
||||
rejectFormRef.value?.resetFields()
|
||||
rejectForm.reject_reason = ''
|
||||
}
|
||||
|
||||
// 审批拒绝
|
||||
const handleRejectRefund = async () => {
|
||||
if (!rejectFormRef.value || !refund.value) return
|
||||
|
||||
await rejectFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
rejectLoading.value = true
|
||||
try {
|
||||
await RefundService.rejectRefund(refund.value.id, rejectForm)
|
||||
ElMessage.success('审批拒绝成功')
|
||||
rejectDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
rejectLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示退回对话框
|
||||
const handleShowReturn = () => {
|
||||
returnDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 退回对话框关闭后的清理
|
||||
const handleReturnDialogClosed = () => {
|
||||
returnFormRef.value?.resetFields()
|
||||
returnForm.remark = ''
|
||||
}
|
||||
|
||||
// 退回
|
||||
const handleReturnRefund = async () => {
|
||||
if (!returnFormRef.value || !refund.value) return
|
||||
|
||||
await returnFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
returnLoading.value = true
|
||||
try {
|
||||
await RefundService.returnRefund(refund.value.id, returnForm)
|
||||
ElMessage.success('退回成功')
|
||||
returnDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
returnLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 显示重新提交对话框
|
||||
const handleShowResubmit = () => {
|
||||
if (!refund.value) return
|
||||
// 预填充原有数据
|
||||
resubmitForm.requested_refund_amount = refund.value.requested_refund_amount / 100
|
||||
resubmitForm.actual_received_amount = refund.value.actual_received_amount / 100
|
||||
resubmitForm.refund_reason = refund.value.refund_reason
|
||||
resubmitDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 重新提交对话框关闭后的清理
|
||||
const handleResubmitDialogClosed = () => {
|
||||
resubmitFormRef.value?.resetFields()
|
||||
resubmitForm.requested_refund_amount = undefined
|
||||
resubmitForm.actual_received_amount = undefined
|
||||
resubmitForm.refund_reason = ''
|
||||
}
|
||||
|
||||
// 重新提交
|
||||
const handleResubmitRefund = async () => {
|
||||
if (!resubmitFormRef.value || !refund.value) return
|
||||
|
||||
await resubmitFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
resubmitLoading.value = true
|
||||
try {
|
||||
const data: ResubmitRefundRequest = {
|
||||
requested_refund_amount: resubmitForm.requested_refund_amount
|
||||
? resubmitForm.requested_refund_amount * 100
|
||||
: undefined,
|
||||
actual_received_amount: resubmitForm.actual_received_amount
|
||||
? resubmitForm.actual_received_amount * 100
|
||||
: undefined,
|
||||
refund_reason: resubmitForm.refund_reason || undefined
|
||||
}
|
||||
|
||||
await RefundService.resubmitRefund(refund.value.id, data)
|
||||
ElMessage.success('重新提交成功')
|
||||
resubmitDialogVisible.value = false
|
||||
await fetchRefundDetail(refund.value.id)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
resubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refund-detail-page {
|
||||
padding: 20px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1038
src/views/finance/refund/index.vue
Normal file
1038
src/views/finance/refund/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
292
src/views/polling-management/alert-history/index.vue
Normal file
292
src/views/polling-management/alert-history/index.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="alert-history-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<el-card shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
/>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { PollingAlertService } from '@/api/modules'
|
||||
import type {
|
||||
PollingAlertHistory,
|
||||
PollingAlertHistoryQueryParams,
|
||||
PollingAlertRule
|
||||
} 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<PollingAlertHistory[]>([])
|
||||
const tableRef = ref()
|
||||
const contextMenuRef = ref()
|
||||
const currentClickRow = ref<PollingAlertHistory | null>(null)
|
||||
const ruleList = ref<PollingAlertRule[]>([])
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
rule_id: undefined as number | undefined
|
||||
})
|
||||
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '告警规则',
|
||||
prop: 'rule_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
placeholder: '请选择告警规则'
|
||||
},
|
||||
options: () =>
|
||||
ruleList.value.map((r) => ({
|
||||
label: r.rule_name,
|
||||
value: r.id
|
||||
}))
|
||||
}
|
||||
])
|
||||
|
||||
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',
|
||||
label: '指标类型',
|
||||
width: 120,
|
||||
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
|
||||
},
|
||||
{
|
||||
prop: 'task_type',
|
||||
label: '任务类型',
|
||||
width: 150,
|
||||
formatter: (row: any) => getTaskTypeLabel(row.task_type)
|
||||
},
|
||||
{
|
||||
prop: 'threshold',
|
||||
label: '阈值',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'current_value',
|
||||
label: '触发值',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'message',
|
||||
label: '告警消息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
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[]>(() => [
|
||||
{
|
||||
label: '刷新',
|
||||
key: 'refresh'
|
||||
}
|
||||
])
|
||||
|
||||
const loadRuleList = async () => {
|
||||
try {
|
||||
const res = await PollingAlertService.getPollingAlertRules()
|
||||
if (res.code === 0 && res.data) {
|
||||
ruleList.value = res.data.items
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载规则列表失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const queryParams: PollingAlertHistoryQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...searchForm
|
||||
}
|
||||
const { data } = await PollingAlertService.getPollingAlertHistory(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, {
|
||||
rule_id: 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 handleRowContextMenu = (row: PollingAlertHistory, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentClickRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (item.key === 'refresh') {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRuleList()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.alert-history-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
479
src/views/polling-management/alert-rules/index.vue
Normal file
479
src/views/polling-management/alert-rules/index.vue
Normal file
@@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="alert-rules-page" id="table-full-screen">
|
||||
<el-card shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<el-button type="primary" @click="showCreateDialog">新增规则</el-button>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑规则弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'create' ? '新增告警规则' : '编辑告警规则'"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规则名称" prop="rule_name">
|
||||
<el-input v-model="form.rule_name" placeholder="请输入规则名称" />
|
||||
</el-form-item>
|
||||
</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-option label="警告" value="warning" />
|
||||
<el-option label="严重" value="critical" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="指标类型" prop="metric_type">
|
||||
<el-select
|
||||
v-model="form.metric_type"
|
||||
placeholder="请选择指标类型"
|
||||
:disabled="dialogType === 'edit'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="队列大小" value="queue_size" />
|
||||
<el-option label="成功率" value="success_rate" />
|
||||
<el-option label="平均执行时间" value="avg_duration" />
|
||||
<el-option label="并发数" value="concurrency" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="任务类型" prop="task_type">
|
||||
<el-select
|
||||
v-model="form.task_type"
|
||||
placeholder="请选择任务类型"
|
||||
:disabled="dialogType === 'edit'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="实名认证轮询" value="polling:realname" />
|
||||
<el-option label="卡数据轮询" value="polling:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="运算符" prop="operator">
|
||||
<el-select v-model="form.operator" placeholder="请选择运算符" style="width: 100%">
|
||||
<el-option label=">" value=">" />
|
||||
<el-option label=">=" value=">=" />
|
||||
<el-option label="<" value="<" />
|
||||
<el-option label="<=" value="<=" />
|
||||
<el-option label="=" value="=" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="阈值" prop="threshold">
|
||||
<el-input-number
|
||||
v-model="form.threshold"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入阈值"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="冷却时间(分钟)" prop="cooldown_minutes">
|
||||
<el-input-number
|
||||
v-model="form.cooldown_minutes"
|
||||
:min="1"
|
||||
placeholder="请输入冷却时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="通知渠道" prop="notify_channels">
|
||||
<el-input
|
||||
v-model="form.notify_channels"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入通知渠道配置(JSON格式)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</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'
|
||||
|
||||
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 form = reactive<CreatePollingAlertRuleRequest & { id?: number }>({
|
||||
rule_name: '',
|
||||
alert_level: '',
|
||||
metric_type: '',
|
||||
task_type: '',
|
||||
threshold: 0,
|
||||
operator: '>',
|
||||
cooldown_minutes: 5,
|
||||
notify_channels: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
rule_name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
alert_level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
|
||||
metric_type: [{ required: true, message: '请选择指标类型', trigger: 'change' }],
|
||||
task_type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
|
||||
operator: [{ required: true, message: '请选择运算符', trigger: 'change' }],
|
||||
cooldown_minutes: [{ required: true, message: '请输入冷却时间', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const getAlertLevelLabel = (level: string) => {
|
||||
return level === 'critical' ? '严重' : '警告'
|
||||
}
|
||||
|
||||
const getMetricTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
queue_size: '队列大小',
|
||||
success_rate: '成功率',
|
||||
avg_duration: '平均执行时间',
|
||||
concurrency: '并发数'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getTaskTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'polling:realname': '实名认证轮询',
|
||||
'polling:carddata': '卡数据轮询',
|
||||
'polling:package': '套餐轮询'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'rule_name',
|
||||
label: '规则名称',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'alert_level',
|
||||
label: '告警级别',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.alert_level === 'critical' ? 'danger' : 'warning' },
|
||||
() => getAlertLevelLabel(row.alert_level)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'metric_type_name',
|
||||
label: '指标类型',
|
||||
width: 120,
|
||||
formatter: (row: any) => getMetricTypeLabel(row.metric_type)
|
||||
},
|
||||
{
|
||||
prop: 'task_type_name',
|
||||
label: '任务类型',
|
||||
width: 150,
|
||||
formatter: (row: any) => getTaskTypeLabel(row.task_type)
|
||||
},
|
||||
{
|
||||
prop: 'operator',
|
||||
label: '运算符',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'threshold',
|
||||
label: '阈值',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'cooldown_minutes',
|
||||
label: '冷却时间(分钟)',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h(ElTag, { type: row.status === 1 ? 'success' : 'info' }, () =>
|
||||
row.status === 1 ? '启用' : '禁用'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.created_at)
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'toggle', label: currentRow.value?.status === 1 ? '禁用' : '启用' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await PollingAlertService.getPollingAlertRules()
|
||||
if (res.code === 0 && res.data) {
|
||||
tableData.value = res.data.items
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
rule_name: '',
|
||||
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(newStatus === 1 ? '已启用' : '已禁用')
|
||||
loadData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
292
src/views/polling-management/concurrency/index.vue
Normal file
292
src/views/polling-management/concurrency/index.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>并发配置</span>
|
||||
<el-button
|
||||
v-permission="'polling_concurrency_reset'"
|
||||
type="warning"
|
||||
@click="handleResetAll"
|
||||
>
|
||||
重置全部并发
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
:row-class-name="getRowClassName"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<el-table-column prop="task_type" label="任务类型" min-width="200" />
|
||||
<el-table-column prop="task_type_name" label="任务名称" min-width="150" />
|
||||
<el-table-column prop="max_concurrency" label="最大并发数" min-width="120" align="center" />
|
||||
<el-table-column prop="current" label="当前并发数" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.current > row.max_concurrency * 0.8 ? 'danger' : 'success'">
|
||||
{{ row.current }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="available" label="可用并发数" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.available === 0 ? 'danger' : 'info'">
|
||||
{{ row.available }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="utilization" label="使用率" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.utilization"
|
||||
:color="getUtilizationColor(row.utilization)"
|
||||
:stroke-width="8"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="修改并发配置"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<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>
|
||||
<el-form-item label="最大并发数" prop="max_concurrency">
|
||||
<el-input-number
|
||||
v-model="formData.max_concurrency"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</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'
|
||||
|
||||
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 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 getUtilizationColor = (percentage: number) => {
|
||||
if (percentage >= 90) return '#f56c6c'
|
||||
if (percentage >= 70) return '#e6a23c'
|
||||
if (percentage >= 50) return '#409eff'
|
||||
return '#67c23a'
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
</style>
|
||||
704
src/views/polling-management/config/index.vue
Normal file
704
src/views/polling-management/config/index.vue
Normal file
@@ -0,0 +1,704 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="polling-config-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="true"
|
||||
label-width="100"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<el-card shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<el-button type="primary" @click="handleCreate" v-permission="'polling_config_create'">
|
||||
新增配置
|
||||
</el-button>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'create' ? '新增轮询配置' : '编辑轮询配置'"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<!-- 基本信息 -->
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="配置名称" prop="config_name">
|
||||
<el-input
|
||||
v-model="formData.config_name"
|
||||
placeholder="请输入配置名称"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="优先级" prop="priority">
|
||||
<el-input-number
|
||||
v-model="formData.priority"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:controls="false"
|
||||
placeholder="数字越小优先级越高"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="配置说明" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入配置说明(最多500字符)"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<el-divider content-position="left">筛选条件</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="卡业务类型" prop="card_category">
|
||||
<el-select
|
||||
v-model="formData.card_category"
|
||||
placeholder="请选择卡业务类型"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option label="普通卡" value="normal" />
|
||||
<el-option label="行业卡" value="industry" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="卡状态条件" prop="card_condition">
|
||||
<el-select
|
||||
v-model="formData.card_condition"
|
||||
placeholder="请选择卡状态条件"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option label="未实名" value="not_real_name" />
|
||||
<el-option label="已实名" value="real_name" />
|
||||
<el-option label="已激活" value="activated" />
|
||||
<el-option label="已停用" value="suspended" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="运营商" prop="carrier_id">
|
||||
<el-select
|
||||
v-model="formData.carrier_id"
|
||||
placeholder="请选择运营商"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="remoteSearchCarrier"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="carrier in carrierList"
|
||||
:key="carrier.id"
|
||||
:label="carrier.carrier_name"
|
||||
:value="carrier.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 检查间隔配置 -->
|
||||
<el-divider content-position="left">检查间隔配置(秒)</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实名检查" prop="realname_check_interval">
|
||||
<el-input-number
|
||||
v-model="formData.realname_check_interval"
|
||||
:min="30"
|
||||
:controls="false"
|
||||
placeholder="最小30"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="套餐检查" prop="package_check_interval">
|
||||
<el-input-number
|
||||
v-model="formData.package_check_interval"
|
||||
:min="60"
|
||||
:controls="false"
|
||||
placeholder="最小60"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="流量检查" prop="carddata_check_interval">
|
||||
<el-input-number
|
||||
v-model="formData.carddata_check_interval"
|
||||
:min="60"
|
||||
:controls="false"
|
||||
placeholder="最小60"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PollingConfigService, CarrierService } from '@/api/modules'
|
||||
import type {
|
||||
PollingConfig,
|
||||
PollingConfigQueryParams,
|
||||
CreatePollingConfigRequest,
|
||||
UpdatePollingConfigRequest
|
||||
} 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<PollingConfig[]>([])
|
||||
const tableRef = ref()
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'create' | 'edit'>('create')
|
||||
const formRef = ref<FormInstance>()
|
||||
const contextMenuRef = ref()
|
||||
const currentClickRow = ref<PollingConfig | null>(null)
|
||||
const carrierList = ref<any[]>([])
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
config_name: '',
|
||||
card_category: undefined as string | undefined,
|
||||
card_condition: undefined as string | undefined,
|
||||
carrier_id: undefined as number | undefined,
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
const formData = reactive<CreatePollingConfigRequest & { id?: number }>({
|
||||
config_name: '',
|
||||
priority: 1,
|
||||
description: '',
|
||||
card_category: undefined,
|
||||
card_condition: undefined,
|
||||
carrier_id: undefined,
|
||||
realname_check_interval: null,
|
||||
package_check_interval: null,
|
||||
carddata_check_interval: null
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
config_name: [
|
||||
{ required: true, message: '请输入配置名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '配置名称长度为1-100字符', trigger: 'blur' }
|
||||
],
|
||||
priority: [
|
||||
{ required: true, message: '请输入优先级', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 1000, message: '优先级范围为1-1000', trigger: 'blur' }
|
||||
],
|
||||
description: [{ max: 500, message: '配置说明最多500字符', trigger: 'blur' }],
|
||||
realname_check_interval: [
|
||||
{ type: 'number', min: 30, message: '实名检查间隔最小30秒', trigger: 'blur' }
|
||||
],
|
||||
package_check_interval: [
|
||||
{ type: 'number', min: 60, message: '套餐检查间隔最小60秒', trigger: 'blur' }
|
||||
],
|
||||
carddata_check_interval: [
|
||||
{ type: 'number', min: 60, message: '流量检查间隔最小60秒', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '配置名称',
|
||||
prop: 'config_name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入配置名称'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '卡业务类型',
|
||||
prop: 'card_category',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择卡业务类型'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '普通卡', value: 'normal' },
|
||||
{ label: '行业卡', value: 'industry' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '卡状态条件',
|
||||
prop: 'card_condition',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择卡状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '未实名', value: 'not_real_name' },
|
||||
{ label: '已实名', value: 'real_name' },
|
||||
{ label: '已激活', value: 'activated' },
|
||||
{ label: '已停用', value: 'suspended' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '运营商',
|
||||
prop: 'carrier_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
remoteMethod: remoteSearchCarrier,
|
||||
placeholder: '请选择运营商'
|
||||
},
|
||||
options: () =>
|
||||
carrierList.value.map((c) => ({
|
||||
label: c.carrier_name,
|
||||
value: c.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
options: () => [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getCardConditionLabel = (condition: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
not_real_name: '未实名',
|
||||
real_name: '已实名',
|
||||
activated: '已激活',
|
||||
suspended: '已停用'
|
||||
}
|
||||
return labels[condition] || condition
|
||||
}
|
||||
|
||||
const getCarrierName = (carrierId: number) => {
|
||||
const carrier = carrierList.value.find((c) => c.id === carrierId)
|
||||
return carrier?.carrier_name || '-'
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'config_name',
|
||||
label: '配置名称',
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: '配置说明',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'card_category',
|
||||
label: '卡业务类型',
|
||||
width: 120,
|
||||
formatter: (row: any) =>
|
||||
row.card_category ? (row.card_category === 'normal' ? '普通卡' : '行业卡') : '-'
|
||||
},
|
||||
{
|
||||
prop: 'card_condition',
|
||||
label: '卡状态条件',
|
||||
width: 120,
|
||||
formatter: (row: any) =>
|
||||
row.card_condition ? getCardConditionLabel(row.card_condition) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'carrier_id',
|
||||
label: '运营商',
|
||||
width: 120,
|
||||
formatter: (row: any) => getCarrierName(row.carrier_id)
|
||||
},
|
||||
{
|
||||
prop: 'priority',
|
||||
label: '优先级',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: any) => (row.status === 1 ? '启用' : '禁用')
|
||||
},
|
||||
{
|
||||
label: '检查间隔(秒)',
|
||||
width: 220,
|
||||
slots: {
|
||||
default: ({ row }: any) =>
|
||||
h('div', { style: 'font-size: 12px; line-height: 1.5' }, [
|
||||
h('div', `实名: ${row.realname_check_interval ?? '-'}`),
|
||||
h('div', `套餐: ${row.package_check_interval ?? '-'}`),
|
||||
h('div', `流量: ${row.carddata_check_interval ?? '-'}`)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: any) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = computed(() =>
|
||||
columns.value.map((col) => ({
|
||||
label: col.label,
|
||||
prop: col.prop || col.label,
|
||||
visible: true
|
||||
}))
|
||||
)
|
||||
|
||||
const contextMenuItems = computed<MenuItemType[]>(() => [
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
permission: 'polling_config_update'
|
||||
},
|
||||
{
|
||||
label: currentClickRow.value?.status === 1 ? '禁用' : '启用',
|
||||
key: 'toggle',
|
||||
permission: 'polling_config_update_status'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
permission: 'polling_config_delete'
|
||||
}
|
||||
])
|
||||
|
||||
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 loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const queryParams: PollingConfigQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
...searchForm
|
||||
}
|
||||
const { data } = await PollingConfigService.getPollingConfigs(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, {
|
||||
config_name: '',
|
||||
card_category: undefined,
|
||||
card_condition: undefined,
|
||||
carrier_id: undefined,
|
||||
status: 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 handleCreate = () => {
|
||||
dialogType.value = 'create'
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
config_name: '',
|
||||
priority: 1,
|
||||
description: '',
|
||||
card_category: undefined,
|
||||
card_condition: undefined,
|
||||
carrier_id: undefined,
|
||||
realname_check_interval: null,
|
||||
package_check_interval: null,
|
||||
carddata_check_interval: null
|
||||
})
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = async (row: PollingConfig) => {
|
||||
try {
|
||||
const { data } = await PollingConfigService.getPollingConfigById(row.id)
|
||||
dialogType.value = 'edit'
|
||||
Object.assign(formData, {
|
||||
id: data.id,
|
||||
config_name: data.config_name,
|
||||
priority: data.priority,
|
||||
description: data.description,
|
||||
card_category: data.card_category,
|
||||
card_condition: data.card_condition,
|
||||
carrier_id: data.carrier_id,
|
||||
realname_check_interval: data.realname_check_interval,
|
||||
package_check_interval: data.package_check_interval,
|
||||
carddata_check_interval: data.carddata_check_interval
|
||||
})
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
} catch (error) {
|
||||
ElMessage.error('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (row: PollingConfig) => {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
try {
|
||||
await PollingConfigService.updatePollingConfigStatus(row.id, { status: newStatus })
|
||||
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: PollingConfig) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除配置 "${row.config_name}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await PollingConfigService.deletePollingConfig(row.id)
|
||||
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 {
|
||||
const submitData: CreatePollingConfigRequest | UpdatePollingConfigRequest = {
|
||||
config_name: formData.config_name,
|
||||
priority: formData.priority,
|
||||
description: formData.description,
|
||||
card_category: formData.card_category,
|
||||
card_condition: formData.card_condition,
|
||||
carrier_id: formData.carrier_id,
|
||||
realname_check_interval: formData.realname_check_interval,
|
||||
package_check_interval: formData.package_check_interval,
|
||||
carddata_check_interval: formData.carddata_check_interval
|
||||
}
|
||||
|
||||
if (dialogType.value === 'create') {
|
||||
await PollingConfigService.createPollingConfig(submitData as CreatePollingConfigRequest)
|
||||
ElMessage.success('创建成功')
|
||||
} else {
|
||||
await PollingConfigService.updatePollingConfig(formData.id!, submitData)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error(dialogType.value === 'create' ? '创建失败' : '更新失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleRowContextMenu = (row: PollingConfig, 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 'toggle':
|
||||
handleToggleStatus(currentClickRow.value)
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentClickRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCarriers()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.polling-config-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
625
src/views/polling-management/data-cleanup/index.vue
Normal file
625
src/views/polling-management/data-cleanup/index.vue
Normal file
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="data-cleanup-page" id="table-full-screen">
|
||||
<!-- 进度提示 -->
|
||||
<ElAlert
|
||||
v-if="progress?.is_running"
|
||||
title="数据清理进行中"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #default>
|
||||
<div>
|
||||
当前清理表: {{ progress.current_table }} | 已处理: {{ progress.processed_tables }}/{{
|
||||
progress.total_tables
|
||||
}}
|
||||
| 已删除: {{ progress.total_deleted }} 条记录
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showCreateDialog">新增配置</ElButton>
|
||||
<ElButton @click="handlePreview">预览清理</ElButton>
|
||||
<ElButton type="warning" @click="handleTriggerCleanup" :loading="triggering">
|
||||
手动清理
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="configList"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:show-pagination="false"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增配置' : '编辑配置'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="表名" prop="table_name">
|
||||
<ElInput
|
||||
v-model="form.table_name"
|
||||
placeholder="请输入表名"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="配置说明" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入配置说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="保留天数" prop="retention_days">
|
||||
<ElInputNumber
|
||||
v-model="form.retention_days"
|
||||
:min="7"
|
||||
:max="3650"
|
||||
placeholder="最少7天"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="每批删除条数" prop="batch_size">
|
||||
<ElInputNumber
|
||||
v-model="form.batch_size"
|
||||
:min="100"
|
||||
:max="100000"
|
||||
placeholder="默认10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'edit'" label="是否启用" prop="enabled">
|
||||
<ElSwitch
|
||||
v-model="form.enabled"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
提交
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<ElDialog v-model="previewVisible" title="预览待清理数据" width="700px">
|
||||
<ElTable :data="previewData" border>
|
||||
<ElTableColumn prop="table_name" label="表名" width="150" />
|
||||
<ElTableColumn prop="description" label="说明" min-width="180" />
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
|
||||
<ElTableColumn prop="record_count" label="待清理记录数" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-danger': row.record_count > 10000 }">
|
||||
{{ row.record_count.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<template #footer>
|
||||
<ElButton @click="previewVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 清理日志对话框 -->
|
||||
<ElDialog v-model="logVisible" title="清理日志" width="1000px">
|
||||
<div style="margin-bottom: 16px">
|
||||
<ElInput
|
||||
v-model="logFilter.table_name"
|
||||
placeholder="表名筛选"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@clear="getCleanupLogs"
|
||||
/>
|
||||
<ElButton type="primary" @click="getCleanupLogs">查询</ElButton>
|
||||
</div>
|
||||
<ElTable :data="logList" border>
|
||||
<ElTableColumn prop="table_name" label="表名" width="120" />
|
||||
<ElTableColumn prop="cleanup_type" label="清理类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.cleanup_type === 'scheduled' ? 'success' : 'warning'">
|
||||
{{ row.cleanup_type === 'scheduled' ? '定时' : '手动' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag
|
||||
:type="
|
||||
row.status === 'success'
|
||||
? 'success'
|
||||
: row.status === 'running'
|
||||
? 'info'
|
||||
: 'danger'
|
||||
"
|
||||
>
|
||||
{{
|
||||
row.status === 'success' ? '成功' : row.status === 'running' ? '运行中' : '失败'
|
||||
}}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
|
||||
<ElTableColumn prop="deleted_count" label="删除记录数" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.deleted_count.toLocaleString() }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="duration_ms" label="耗时(ms)" width="100" align="right" />
|
||||
<ElTableColumn prop="started_at" label="开始时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.started_at) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="error_message"
|
||||
label="错误信息"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</ElTable>
|
||||
<ElPagination
|
||||
v-model:current-page="logPagination.page"
|
||||
v-model:page-size="logPagination.page_size"
|
||||
:total="logPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@size-change="getCleanupLogs"
|
||||
@current-change="getCleanupLogs"
|
||||
/>
|
||||
<template #footer>
|
||||
<ElButton @click="logVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { DataCleanupService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElSwitch, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
DataCleanupConfig,
|
||||
CreateDataCleanupConfigRequest,
|
||||
UpdateDataCleanupConfigRequest,
|
||||
DataCleanupLog,
|
||||
DataCleanupPreviewItem,
|
||||
DataCleanupProgress
|
||||
} from '@/types/api'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'DataCleanup' })
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const triggering = ref(false)
|
||||
const tableRef = ref()
|
||||
const dialogVisible = ref(false)
|
||||
const previewVisible = ref(false)
|
||||
const logVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 右键菜单
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<DataCleanupConfig | null>(null)
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '表名', prop: 'table_name' },
|
||||
{ label: '配置说明', prop: 'description' },
|
||||
{ label: '保留天数', prop: 'retention_days' },
|
||||
{ label: '每批删除条数', prop: 'batch_size' },
|
||||
{ label: '是否启用', prop: 'enabled' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '更新时间', prop: 'updated_at' }
|
||||
]
|
||||
|
||||
// 验证规则
|
||||
const rules: FormRules = {
|
||||
table_name: [{ required: true, message: '请输入表名', trigger: 'blur' }],
|
||||
retention_days: [
|
||||
{ required: true, message: '请输入保留天数', trigger: 'blur' },
|
||||
{ type: 'number', min: 7, message: '保留天数最少7天', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
table_name: '',
|
||||
description: '',
|
||||
retention_days: 30,
|
||||
batch_size: 10000,
|
||||
enabled: 1
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.id = 0
|
||||
form.table_name = ''
|
||||
form.description = ''
|
||||
form.retention_days = 30
|
||||
form.batch_size = 10000
|
||||
form.enabled = 1
|
||||
}
|
||||
|
||||
const configList = ref<DataCleanupConfig[]>([])
|
||||
const previewData = ref<DataCleanupPreviewItem[]>([])
|
||||
const logList = ref<DataCleanupLog[]>([])
|
||||
const progress = ref<DataCleanupProgress | null>(null)
|
||||
|
||||
// 日志筛选和分页
|
||||
const logFilter = reactive({
|
||||
table_name: ''
|
||||
})
|
||||
|
||||
const logPagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'table_name',
|
||||
label: '表名',
|
||||
width: 200,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: '配置说明',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'retention_days',
|
||||
label: '保留天数',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'batch_size',
|
||||
label: '每批删除条数',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
formatter: (row: DataCleanupConfig) => row.batch_size.toLocaleString()
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '是否启用',
|
||||
width: 100,
|
||||
formatter: (row: DataCleanupConfig) => {
|
||||
return h(ElTag, { type: row.enabled === 1 ? 'success' : 'info' }, () =>
|
||||
row.enabled === 1 ? '启用' : '禁用'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: DataCleanupConfig) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: DataCleanupConfig) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
getProgress()
|
||||
// 每30秒刷新一次进度
|
||||
setInterval(getProgress, 30000)
|
||||
})
|
||||
|
||||
// 获取配置列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await DataCleanupService.getDataCleanupConfigs()
|
||||
if (res.code === 0) {
|
||||
configList.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进度
|
||||
const getProgress = async () => {
|
||||
try {
|
||||
const res = await DataCleanupService.getDataCleanupProgress()
|
||||
if (res.code === 0) {
|
||||
progress.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
getProgress()
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
resetForm()
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = async (row: DataCleanupConfig) => {
|
||||
try {
|
||||
// 先调用详情接口获取完整数据
|
||||
const res = await DataCleanupService.getDataCleanupConfigById(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
Object.assign(form, {
|
||||
id: res.data.id,
|
||||
table_name: res.data.table_name,
|
||||
description: res.data.description || '',
|
||||
retention_days: res.data.retention_days,
|
||||
batch_size: res.data.batch_size,
|
||||
enabled: res.data.enabled
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
ElMessage.error('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框关闭
|
||||
const handleDialogClosed = () => {
|
||||
// 对话框关闭后不做任何操作
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const data: CreateDataCleanupConfigRequest = {
|
||||
table_name: form.table_name,
|
||||
retention_days: form.retention_days,
|
||||
batch_size: form.batch_size || undefined,
|
||||
description: form.description || undefined
|
||||
}
|
||||
await DataCleanupService.createDataCleanupConfig(data)
|
||||
ElMessage.success('创建成功')
|
||||
} else {
|
||||
const data: UpdateDataCleanupConfigRequest = {
|
||||
retention_days: form.retention_days,
|
||||
batch_size: form.batch_size,
|
||||
description: form.description || undefined,
|
||||
enabled: form.enabled
|
||||
}
|
||||
await DataCleanupService.updateDataCleanupConfig(form.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const handleDelete = async (row: DataCleanupConfig) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除配置"${row.table_name}"吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await DataCleanupService.deleteDataCleanupConfig(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览清理
|
||||
const handlePreview = async () => {
|
||||
try {
|
||||
const res = await DataCleanupService.previewDataCleanup()
|
||||
if (res.code === 0) {
|
||||
previewData.value = res.data.items || []
|
||||
previewVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动触发清理
|
||||
const handleTriggerCleanup = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要手动触发数据清理吗?', '确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
triggering.value = true
|
||||
await DataCleanupService.triggerDataCleanup()
|
||||
ElMessage.success('已触发清理任务')
|
||||
await getProgress()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
} finally {
|
||||
triggering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看日志
|
||||
const handleViewLogs = () => {
|
||||
logFilter.table_name = ''
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
}
|
||||
|
||||
// 获取清理日志
|
||||
const getCleanupLogs = async () => {
|
||||
try {
|
||||
const res = await DataCleanupService.getDataCleanupLogs({
|
||||
table_name: logFilter.table_name || undefined,
|
||||
page: logPagination.page,
|
||||
page_size: logPagination.page_size
|
||||
})
|
||||
if (res.code === 0) {
|
||||
logList.value = res.data.items || []
|
||||
logPagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'logs', label: '查看日志' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: DataCleanupConfig, 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 'logs':
|
||||
logFilter.table_name = currentRow.value.table_name
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.data-cleanup-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
763
src/views/polling-management/manual-trigger/index.vue
Normal file
763
src/views/polling-management/manual-trigger/index.vue
Normal file
@@ -0,0 +1,763 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="manual-trigger-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
label-width="100"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<el-card shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="showBatchTriggerDialog"
|
||||
v-permission="'polling_manual_trigger_batch'"
|
||||
>
|
||||
批量触发
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="showConditionTriggerDialog"
|
||||
v-permission="'polling_manual_trigger_condition'"
|
||||
>
|
||||
条件触发
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
@click="showSingleTriggerDialog"
|
||||
v-permission="'polling_manual_trigger_single'"
|
||||
>
|
||||
单卡触发
|
||||
</el-button>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="120"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 批量触发弹窗 -->
|
||||
<el-dialog
|
||||
v-model="batchDialogVisible"
|
||||
title="批量触发"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<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-option label="实名认证轮询" value="polling:realname" />
|
||||
<el-option label="卡数据轮询" value="polling:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="卡ID列表" prop="card_ids">
|
||||
<el-input
|
||||
v-model="batchForm.card_ids_text"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入卡ID,多个ID用逗号或换行分隔(最多1000个)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleBatchSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 条件触发弹窗 -->
|
||||
<el-dialog
|
||||
v-model="conditionDialogVisible"
|
||||
title="条件触发"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="conditionFormRef"
|
||||
:model="conditionForm"
|
||||
:rules="conditionRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="任务类型" prop="task_type">
|
||||
<el-select
|
||||
v-model="conditionForm.task_type"
|
||||
placeholder="请选择任务类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="实名认证轮询" value="polling:realname" />
|
||||
<el-option label="卡数据轮询" value="polling:carddata" />
|
||||
<el-option label="套餐轮询" value="polling:package" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="限制数量" prop="limit">
|
||||
<el-input-number
|
||||
v-model="conditionForm.limit"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
placeholder="最多1000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="卡状态">
|
||||
<el-select
|
||||
v-model="conditionForm.card_status"
|
||||
placeholder="请选择卡状态"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="未激活" value="inactive" />
|
||||
<el-option label="已激活" value="activated" />
|
||||
<el-option label="已停用" value="suspended" />
|
||||
<el-option label="已注销" value="terminated" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="卡类型">
|
||||
<el-select
|
||||
v-model="conditionForm.card_type"
|
||||
placeholder="请选择卡类型"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="普通卡" value="normal" />
|
||||
<el-option label="行业卡" value="industry" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="运营商">
|
||||
<el-select
|
||||
v-model="conditionForm.carrier_code"
|
||||
placeholder="请选择运营商"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="remoteSearchCarrier"
|
||||
clearable
|
||||
no-data-text="暂无运营商数据"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="carrier in carrierList"
|
||||
:key="carrier.id"
|
||||
:label="carrier.carrier_name"
|
||||
:value="carrier.carrier_code"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="店铺">
|
||||
<el-tree-select
|
||||
v-model="conditionForm.shop_id"
|
||||
:data="shopTreeData"
|
||||
placeholder="请选择店铺"
|
||||
filterable
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
no-data-text="暂无店铺数据"
|
||||
:props="{
|
||||
label: 'shop_name',
|
||||
value: 'id',
|
||||
children: 'children'
|
||||
}"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用轮询">
|
||||
<el-switch v-model="conditionForm.enable_polling" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="conditionDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConditionSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 单卡触发弹窗 -->
|
||||
<el-dialog
|
||||
v-model="singleDialogVisible"
|
||||
title="单卡触发"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="singleFormRef" :model="singleForm" :rules="singleRules" label-width="100px">
|
||||
<el-form-item label="卡ICCID" prop="card_id">
|
||||
<el-select
|
||||
v-model="singleForm.card_id"
|
||||
placeholder="请输入ICCID搜索"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="remoteSearchCard"
|
||||
clearable
|
||||
no-data-text="暂无卡数据,请输入ICCID搜索"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="card in cardList"
|
||||
:key="card.id"
|
||||
:label="card.iccid"
|
||||
:value="card.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" prop="task_type">
|
||||
<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" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="singleDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSingleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</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'
|
||||
|
||||
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 batchFormRef = ref<FormInstance>()
|
||||
const conditionFormRef = ref<FormInstance>()
|
||||
const singleFormRef = ref<FormInstance>()
|
||||
|
||||
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
|
||||
})
|
||||
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) {
|
||||
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()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.manual-trigger-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
369
src/views/polling-management/monitor/index.vue
Normal file
369
src/views/polling-management/monitor/index.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="polling-monitor-page">
|
||||
<!-- 总览统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon total">
|
||||
<el-icon :size="32"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">总卡数</div>
|
||||
<div class="stat-value">{{ overviewStats?.total_cards || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon initialized">
|
||||
<el-icon :size="32"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">已初始化</div>
|
||||
<div class="stat-value">{{ overviewStats?.initialized_cards || 0 }}</div>
|
||||
<el-progress
|
||||
v-if="overviewStats"
|
||||
:percentage="overviewStats.init_progress"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
style="margin-top: 8px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon realname">
|
||||
<el-icon :size="32"><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">实名队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.realname_queue_size || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon package">
|
||||
<el-icon :size="32"><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">套餐队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.package_queue_size || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" :xl="24 / 5">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon carddata">
|
||||
<el-icon :size="32"><DataLine /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">流量队列</div>
|
||||
<div class="stat-value">{{ overviewStats?.carddata_queue_size || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 队列状态 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>队列状态</span>
|
||||
<el-button size="small" @click="loadQueueStatus">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="queueStatusList" border stripe>
|
||||
<el-table-column prop="task_type_name" label="任务类型" min-width="120" />
|
||||
<el-table-column prop="queue_size" label="队列大小" min-width="100" />
|
||||
<el-table-column prop="due_count" label="到期待处理" min-width="110" />
|
||||
<el-table-column prop="manual_pending" label="手动待处理" min-width="110" />
|
||||
<el-table-column label="平均等待时间" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.avg_wait_time_s ? row.avg_wait_time_s.toFixed(1) + 's' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 任务统计 -->
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>任务统计(最近1小时)</span>
|
||||
<el-button size="small" @click="loadTaskStats">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="taskStatsList" border stripe>
|
||||
<el-table-column prop="task_type_name" label="任务类型" min-width="120" />
|
||||
<el-table-column prop="total_count_1h" label="总数" min-width="100" />
|
||||
<el-table-column prop="success_count_1h" label="成功数" min-width="100" />
|
||||
<el-table-column prop="failure_count_1h" label="失败数" min-width="100" />
|
||||
<el-table-column label="成功率" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress
|
||||
:percentage="row.success_rate"
|
||||
:stroke-width="8"
|
||||
:color="getSuccessRateColor(row.success_rate)"
|
||||
>
|
||||
<span style="font-size: 12px">{{ row.success_rate.toFixed(2) }}%</span>
|
||||
</el-progress>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平均耗时" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.avg_duration_ms ? row.avg_duration_ms.toFixed(1) + 'ms' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 初始化进度 -->
|
||||
<el-card shadow="never" class="section-card" v-if="initProgress && !initProgress.is_complete">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>初始化进度</span>
|
||||
<el-button size="small" @click="loadInitProgress">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="init-progress-content">
|
||||
<div class="progress-info">
|
||||
<div class="info-item">
|
||||
<span class="label">总卡数:</span>
|
||||
<span class="value">{{ initProgress.total_cards }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">已初始化:</span>
|
||||
<span class="value">{{ initProgress.initialized_cards }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">开始时间:</span>
|
||||
<span class="value">{{ formatDateTime(initProgress.started_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">预计完成:</span>
|
||||
<span class="value">{{ formatDateTime(initProgress.estimated_eta) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="initProgress.progress"
|
||||
:stroke-width="20"
|
||||
:color="getProgressColor(initProgress.progress)"
|
||||
>
|
||||
<span style="font-size: 14px; font-weight: bold">{{ initProgress.progress.toFixed(1) }}%</span>
|
||||
</el-progress>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Document, CircleCheck, User, Box, DataLine } from '@element-plus/icons-vue'
|
||||
import { PollingMonitorService } from '@/api/modules'
|
||||
import type {
|
||||
PollingOverviewStats,
|
||||
PollingInitProgress,
|
||||
PollingQueueStatus,
|
||||
PollingTaskStats
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
const overviewStats = ref<PollingOverviewStats>()
|
||||
const initProgress = ref<PollingInitProgress>()
|
||||
const queueStatusList = ref<PollingQueueStatus[]>([])
|
||||
const taskStatsList = ref<PollingTaskStats[]>([])
|
||||
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 加载总览统计
|
||||
const loadOverviewStats = async () => {
|
||||
try {
|
||||
const { data } = await PollingMonitorService.getOverviewStats()
|
||||
overviewStats.value = data
|
||||
} catch (error) {
|
||||
ElMessage.error('加载总览统计失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载初始化进度
|
||||
const loadInitProgress = async () => {
|
||||
try {
|
||||
const { data } = await PollingMonitorService.getInitProgress()
|
||||
initProgress.value = data
|
||||
} catch (error) {
|
||||
console.error('加载初始化进度失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载队列状态
|
||||
const loadQueueStatus = async (showMessage = false) => {
|
||||
try {
|
||||
const { data } = await PollingMonitorService.getQueueStatus()
|
||||
queueStatusList.value = data.items
|
||||
if (showMessage) {
|
||||
ElMessage.success('队列状态已刷新')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载队列状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载任务统计
|
||||
const loadTaskStats = async (showMessage = false) => {
|
||||
try {
|
||||
const { data } = await PollingMonitorService.getTaskStats()
|
||||
taskStatsList.value = data.items
|
||||
if (showMessage) {
|
||||
ElMessage.success('任务统计已刷新')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载任务统计失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载所有数据
|
||||
const loadAllData = () => {
|
||||
loadOverviewStats()
|
||||
loadInitProgress()
|
||||
loadQueueStatus()
|
||||
loadTaskStats()
|
||||
}
|
||||
|
||||
// 获取成功率颜色
|
||||
const getSuccessRateColor = (rate: number) => {
|
||||
if (rate >= 95) return '#67c23a'
|
||||
if (rate >= 85) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
// 获取进度颜色
|
||||
const getProgressColor = (progress: number) => {
|
||||
if (progress >= 90) return '#67c23a'
|
||||
if (progress >= 50) return '#409eff'
|
||||
return '#e6a23c'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllData()
|
||||
|
||||
// 每30秒自动刷新
|
||||
refreshTimer = setInterval(() => {
|
||||
loadAllData()
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.polling-monitor-page {
|
||||
padding: 20px;
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
|
||||
&.total {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
&.initialized {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
&.realname {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
&.package {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
}
|
||||
|
||||
&.carddata {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.init-progress-content {
|
||||
.progress-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.info-item {
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -60,10 +60,11 @@
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增支付配置' : '编辑支付配置'"
|
||||
width="800px"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="140px" class="config-form">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="配置名称" prop="name">
|
||||
@@ -77,6 +78,7 @@
|
||||
placeholder="请选择支付渠道类型"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
@change="handleProviderTypeChange"
|
||||
>
|
||||
<ElOption label="微信直连" value="wechat" />
|
||||
<ElOption label="富友支付" value="fuiou" />
|
||||
@@ -95,11 +97,17 @@
|
||||
|
||||
<!-- 微信相关配置 -->
|
||||
<template v-if="form.provider_type === 'wechat'">
|
||||
<ElDivider content-position="left">小程序配置</ElDivider>
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">小程序配置</span>
|
||||
</ElDivider>
|
||||
<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"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -114,11 +122,17 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDivider content-position="left">公众号配置</ElDivider>
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">公众号配置</span>
|
||||
</ElDivider>
|
||||
<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"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
@@ -155,19 +169,33 @@
|
||||
</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回调地址"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">微信支付配置</ElDivider>
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">微信支付配置</span>
|
||||
</ElDivider>
|
||||
<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="请输入微信商户号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</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="请输入微信证书序列号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
@@ -194,7 +222,11 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="支付回调地址" prop="wx_notify_url">
|
||||
<ElInput v-model="form.wx_notify_url" placeholder="请输入微信支付回调地址" />
|
||||
<ElInput
|
||||
v-model="form.wx_notify_url"
|
||||
placeholder="请输入微信支付回调地址"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
@@ -222,33 +254,207 @@
|
||||
|
||||
<!-- 富友支付配置 -->
|
||||
<template v-if="form.provider_type === 'fuiou'">
|
||||
<ElDivider content-position="left">富友支付配置</ElDivider>
|
||||
<ElDivider content-position="left">小程序配置</ElDivider>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="小程序AppID" prop="miniapp_app_id">
|
||||
<ElInput
|
||||
v-model="form.miniapp_app_id"
|
||||
placeholder="请输入小程序AppID"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="小程序AppSecret" prop="miniapp_app_secret">
|
||||
<ElInput
|
||||
v-model="form.miniapp_app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入小程序AppSecret"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">公众号配置</span>
|
||||
</ElDivider>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公众号AppID" prop="oa_app_id">
|
||||
<ElInput
|
||||
v-model="form.oa_app_id"
|
||||
placeholder="请输入公众号AppID"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公众号AppSecret" prop="oa_app_secret">
|
||||
<ElInput
|
||||
v-model="form.oa_app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入公众号AppSecret"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公众号Token" prop="oa_token">
|
||||
<ElInput
|
||||
v-model="form.oa_token"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入公众号Token"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="公众号AES Key" prop="oa_aes_key">
|
||||
<ElInput
|
||||
v-model="form.oa_aes_key"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入公众号AES加密Key"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
|
||||
<ElInput
|
||||
v-model="form.oa_oauth_redirect_url"
|
||||
placeholder="请输入OAuth回调地址"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">微信支付配置</span>
|
||||
</ElDivider>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="微信商户号" prop="wx_mch_id">
|
||||
<ElInput
|
||||
v-model="form.wx_mch_id"
|
||||
placeholder="请输入微信商户号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="证书序列号" prop="wx_serial_no">
|
||||
<ElInput
|
||||
v-model="form.wx_serial_no"
|
||||
placeholder="请输入微信证书序列号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="APIv2密钥" prop="wx_api_v2_key">
|
||||
<ElInput
|
||||
v-model="form.wx_api_v2_key"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入微信APIv2密钥"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="APIv3密钥" prop="wx_api_v3_key">
|
||||
<ElInput
|
||||
v-model="form.wx_api_v3_key"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入微信APIv3密钥"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElFormItem label="支付回调地址" prop="wx_notify_url">
|
||||
<ElInput
|
||||
v-model="form.wx_notify_url"
|
||||
placeholder="请输入微信支付回调地址"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="支付证书" prop="wx_cert_content">
|
||||
<ElInput
|
||||
v-model="form.wx_cert_content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请粘贴PEM格式证书内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="支付密钥" prop="wx_key_content">
|
||||
<ElInput
|
||||
v-model="form.wx_key_content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请粘贴PEM格式密钥内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDivider content-position="left">
|
||||
<span class="divider-title">富友支付配置</span>
|
||||
</ElDivider>
|
||||
<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地址"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</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="请输入富友机构号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</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="请输入富友商户号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</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="请输入富友终端号"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</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="请输入富友支付回调地址"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
@@ -289,7 +495,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { h, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { WechatConfigService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
|
||||
@@ -387,51 +593,73 @@
|
||||
{ label: '更新时间', prop: 'updated_at' }
|
||||
]
|
||||
|
||||
// 动态表单验证规则
|
||||
const rules = computed<FormRules>(() => {
|
||||
const baseRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||
provider_type: [{ required: true, message: '请选择支付渠道类型', trigger: 'change' }]
|
||||
// URL格式验证规则
|
||||
const urlValidator = (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const urlPattern = /^(https?:\/\/)[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?$/
|
||||
if (!urlPattern.test(value)) {
|
||||
callback(new Error('请输入正确的URL格式(http://或https://)'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 只在创建模式下添加必填验证
|
||||
if (dialogType.value === 'add') {
|
||||
if (form.provider_type === 'wechat') {
|
||||
// 微信直连必填字段
|
||||
baseRules.wx_mch_id = [{ required: true, message: '请输入微信商户号', trigger: 'blur' }]
|
||||
baseRules.wx_api_v3_key = [
|
||||
{ required: true, message: '请输入微信APIv3密钥', trigger: 'blur' }
|
||||
]
|
||||
baseRules.wx_cert_content = [
|
||||
{ required: true, message: '请输入微信支付证书内容', trigger: 'blur' }
|
||||
]
|
||||
baseRules.wx_key_content = [
|
||||
{ required: true, message: '请输入微信支付密钥内容', trigger: 'blur' }
|
||||
]
|
||||
baseRules.wx_serial_no = [
|
||||
{ required: true, message: '请输入微信证书序列号', trigger: 'blur' }
|
||||
]
|
||||
baseRules.wx_notify_url = [
|
||||
{ required: true, message: '请输入微信支付回调地址', trigger: 'blur' }
|
||||
]
|
||||
} else if (form.provider_type === 'fuiou') {
|
||||
// 富友支付必填字段
|
||||
baseRules.fy_api_url = [{ required: true, message: '请输入富友API地址', trigger: 'blur' }]
|
||||
baseRules.fy_ins_cd = [{ required: true, message: '请输入富友机构号', trigger: 'blur' }]
|
||||
baseRules.fy_mchnt_cd = [{ required: true, message: '请输入富友商户号', trigger: 'blur' }]
|
||||
baseRules.fy_term_id = [{ required: true, message: '请输入富友终端号', trigger: 'blur' }]
|
||||
baseRules.fy_private_key = [{ required: true, message: '请输入富友私钥', trigger: 'blur' }]
|
||||
baseRules.fy_public_key = [{ required: true, message: '请输入富友公钥', trigger: 'blur' }]
|
||||
baseRules.fy_notify_url = [
|
||||
{ required: true, message: '请输入富友支付回调地址', trigger: 'blur' }
|
||||
]
|
||||
// 自定义验证器 - 编辑模式下允许为空,新增模式下必填
|
||||
const createRequiredValidator = (fieldName: string) => {
|
||||
return (rule: any, value: any, callback: any) => {
|
||||
if (dialogType.value === 'add' && !value) {
|
||||
callback(new Error(`请输入${fieldName}`))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return baseRules
|
||||
// 静态验证规则 - 一次性定义所有规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||
provider_type: [{ required: true, message: '请选择支付渠道类型', trigger: 'change' }],
|
||||
// 所有字段的验证规则都定义好
|
||||
miniapp_app_id: [{ required: true, validator: createRequiredValidator('小程序AppID'), trigger: 'blur' }],
|
||||
miniapp_app_secret: [{ required: true, validator: createRequiredValidator('小程序AppSecret'), trigger: 'blur' }],
|
||||
oa_app_id: [{ required: true, validator: createRequiredValidator('公众号AppID'), trigger: 'blur' }],
|
||||
oa_app_secret: [{ required: true, validator: createRequiredValidator('公众号AppSecret'), trigger: 'blur' }],
|
||||
oa_token: [{ required: true, validator: createRequiredValidator('公众号Token'), trigger: 'blur' }],
|
||||
oa_aes_key: [{ required: true, validator: createRequiredValidator('公众号AES Key'), trigger: 'blur' }],
|
||||
oa_oauth_redirect_url: [
|
||||
{ required: true, validator: createRequiredValidator('OAuth回调地址'), trigger: 'blur' },
|
||||
{ validator: urlValidator, trigger: 'blur' }
|
||||
],
|
||||
wx_mch_id: [{ required: true, validator: createRequiredValidator('微信商户号'), trigger: 'blur' }],
|
||||
wx_api_v2_key: [{ required: true, validator: createRequiredValidator('微信APIv2密钥'), trigger: 'blur' }],
|
||||
wx_api_v3_key: [{ required: true, validator: createRequiredValidator('微信APIv3密钥'), trigger: 'blur' }],
|
||||
wx_serial_no: [{ required: true, validator: createRequiredValidator('微信证书序列号'), trigger: 'blur' }],
|
||||
wx_cert_content: [{ required: true, validator: createRequiredValidator('微信支付证书内容'), trigger: 'blur' }],
|
||||
wx_key_content: [{ required: true, validator: createRequiredValidator('微信支付密钥内容'), trigger: 'blur' }],
|
||||
wx_notify_url: [
|
||||
{ required: true, validator: createRequiredValidator('微信支付回调地址'), trigger: 'blur' },
|
||||
{ validator: urlValidator, trigger: 'blur' }
|
||||
],
|
||||
fy_api_url: [
|
||||
{ required: true, validator: createRequiredValidator('富友API地址'), trigger: 'blur' },
|
||||
{ validator: urlValidator, trigger: 'blur' }
|
||||
],
|
||||
fy_ins_cd: [{ required: true, validator: createRequiredValidator('富友机构号'), trigger: 'blur' }],
|
||||
fy_mchnt_cd: [{ required: true, validator: createRequiredValidator('富友商户号'), trigger: 'blur' }],
|
||||
fy_term_id: [{ required: true, validator: createRequiredValidator('富友终端号'), trigger: 'blur' }],
|
||||
fy_private_key: [{ required: true, validator: createRequiredValidator('富友私钥'), trigger: 'blur' }],
|
||||
fy_public_key: [{ required: true, validator: createRequiredValidator('富友公钥'), trigger: 'blur' }],
|
||||
fy_notify_url: [
|
||||
{ required: true, validator: createRequiredValidator('富友支付回调地址'), trigger: 'blur' },
|
||||
{ validator: urlValidator, trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const initialFormState = {
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
provider_type: 'wechat' as PaymentProviderType,
|
||||
description: '',
|
||||
@@ -456,9 +684,36 @@
|
||||
fy_notify_url: '',
|
||||
fy_private_key: '',
|
||||
fy_public_key: ''
|
||||
}
|
||||
})
|
||||
|
||||
const form = reactive({ id: 0, ...initialFormState })
|
||||
// 重置表单数据
|
||||
const resetForm = () => {
|
||||
form.id = 0
|
||||
form.name = ''
|
||||
form.provider_type = 'wechat'
|
||||
form.description = ''
|
||||
form.miniapp_app_id = ''
|
||||
form.miniapp_app_secret = ''
|
||||
form.oa_app_id = ''
|
||||
form.oa_app_secret = ''
|
||||
form.oa_token = ''
|
||||
form.oa_aes_key = ''
|
||||
form.oa_oauth_redirect_url = ''
|
||||
form.wx_mch_id = ''
|
||||
form.wx_api_v2_key = ''
|
||||
form.wx_api_v3_key = ''
|
||||
form.wx_notify_url = ''
|
||||
form.wx_serial_no = ''
|
||||
form.wx_cert_content = ''
|
||||
form.wx_key_content = ''
|
||||
form.fy_api_url = ''
|
||||
form.fy_ins_cd = ''
|
||||
form.fy_mchnt_cd = ''
|
||||
form.fy_term_id = ''
|
||||
form.fy_notify_url = ''
|
||||
form.fy_private_key = ''
|
||||
form.fy_public_key = ''
|
||||
}
|
||||
|
||||
const configList = ref<WechatConfig[]>([])
|
||||
|
||||
@@ -583,16 +838,6 @@
|
||||
}
|
||||
])
|
||||
|
||||
// 监听支付渠道类型变化,清除表单验证
|
||||
watch(
|
||||
() => form.provider_type,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
@@ -650,45 +895,72 @@
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
resetForm()
|
||||
dialogType.value = 'add'
|
||||
Object.assign(form, { id: 0, ...initialFormState })
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 处理敏感字段值 - 如果是***或[已配置]等特殊值,转换为空字符串
|
||||
const processSensitiveField = (value: string | undefined | null): string => {
|
||||
if (!value || value === '***' || value.includes('[已配置]') || value.includes('[未配置]')) {
|
||||
return ''
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (row: WechatConfig) => {
|
||||
dialogType.value = 'edit'
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
provider_type: row.provider_type,
|
||||
description: row.description,
|
||||
miniapp_app_id: row.miniapp_app_id,
|
||||
miniapp_app_secret: '',
|
||||
oa_app_id: row.oa_app_id,
|
||||
oa_app_secret: '',
|
||||
oa_token: '',
|
||||
oa_aes_key: '',
|
||||
oa_oauth_redirect_url: row.oa_oauth_redirect_url,
|
||||
wx_mch_id: row.wx_mch_id,
|
||||
wx_api_v2_key: '',
|
||||
wx_api_v3_key: '',
|
||||
wx_notify_url: row.wx_notify_url,
|
||||
wx_serial_no: row.wx_serial_no,
|
||||
wx_cert_content: '',
|
||||
wx_key_content: '',
|
||||
fy_api_url: row.fy_api_url,
|
||||
fy_ins_cd: row.fy_ins_cd,
|
||||
fy_mchnt_cd: row.fy_mchnt_cd,
|
||||
fy_term_id: row.fy_term_id,
|
||||
fy_notify_url: row.fy_notify_url,
|
||||
fy_private_key: '',
|
||||
fy_public_key: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
const showEditDialog = async (row: WechatConfig) => {
|
||||
// 先调用详情接口获取完整数据
|
||||
try {
|
||||
const res = await WechatConfigService.getWechatConfigById(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
const detail = res.data
|
||||
// 赋值表单数据
|
||||
Object.assign(form, {
|
||||
id: detail.id,
|
||||
name: detail.name,
|
||||
provider_type: detail.provider_type,
|
||||
description: detail.description || '',
|
||||
miniapp_app_id: detail.miniapp_app_id || '',
|
||||
miniapp_app_secret: processSensitiveField(detail.miniapp_app_secret),
|
||||
oa_app_id: detail.oa_app_id || '',
|
||||
oa_app_secret: processSensitiveField(detail.oa_app_secret),
|
||||
oa_token: processSensitiveField(detail.oa_token),
|
||||
oa_aes_key: processSensitiveField(detail.oa_aes_key),
|
||||
oa_oauth_redirect_url: detail.oa_oauth_redirect_url || '',
|
||||
wx_mch_id: detail.wx_mch_id || '',
|
||||
wx_api_v2_key: processSensitiveField(detail.wx_api_v2_key),
|
||||
wx_api_v3_key: processSensitiveField(detail.wx_api_v3_key),
|
||||
wx_notify_url: detail.wx_notify_url || '',
|
||||
wx_serial_no: detail.wx_serial_no || '',
|
||||
wx_cert_content: processSensitiveField(detail.wx_cert_content),
|
||||
wx_key_content: processSensitiveField(detail.wx_key_content),
|
||||
fy_api_url: detail.fy_api_url || '',
|
||||
fy_ins_cd: detail.fy_ins_cd || '',
|
||||
fy_mchnt_cd: detail.fy_mchnt_cd || '',
|
||||
fy_term_id: detail.fy_term_id || '',
|
||||
fy_notify_url: detail.fy_notify_url || '',
|
||||
fy_private_key: processSensitiveField(detail.fy_private_key),
|
||||
fy_public_key: processSensitiveField(detail.fy_public_key)
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
ElMessage.error('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理支付渠道类型变化
|
||||
const handleProviderTypeChange = () => {
|
||||
// 清除验证状态
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
@@ -696,8 +968,8 @@
|
||||
|
||||
// 对话框关闭后的清理
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, { id: 0, ...initialFormState })
|
||||
// 不调用resetFields,因为会把数据也重置了
|
||||
// 只需要什么都不做,下次打开时会重新赋值
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
@@ -772,7 +1044,6 @@
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
formRef.value?.resetFields()
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -898,4 +1169,46 @@
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
|
||||
.divider-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 24px 0 20px 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化滚动条样式
|
||||
.config-form::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.config-form::-webkit-scrollbar-thumb {
|
||||
background-color: var(--el-border-color-darker);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.config-form::-webkit-scrollbar-track {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user