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 { AgentRechargeService } from './agentRecharge'
|
||||||
export { WechatConfigService } from './wechatConfig'
|
export { WechatConfigService } from './wechatConfig'
|
||||||
export { ExchangeService } from './exchange'
|
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: 按需添加其他业务模块
|
// TODO: 按需添加其他业务模块
|
||||||
// export { SettingService } from './setting'
|
// 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,
|
isHide: true,
|
||||||
keepAlive: false
|
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
|
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||||
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
||||||
WechatConfig = '/settings/wechat-config', // 微信支付配置
|
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 './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
|
<ElDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增支付配置' : '编辑支付配置'"
|
:title="dialogType === 'add' ? '新增支付配置' : '编辑支付配置'"
|
||||||
width="800px"
|
width="900px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
@closed="handleDialogClosed"
|
@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">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="配置名称" prop="name">
|
<ElFormItem label="配置名称" prop="name">
|
||||||
@@ -77,6 +78,7 @@
|
|||||||
placeholder="请选择支付渠道类型"
|
placeholder="请选择支付渠道类型"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
:disabled="dialogType === 'edit'"
|
||||||
|
@change="handleProviderTypeChange"
|
||||||
>
|
>
|
||||||
<ElOption label="微信直连" value="wechat" />
|
<ElOption label="微信直连" value="wechat" />
|
||||||
<ElOption label="富友支付" value="fuiou" />
|
<ElOption label="富友支付" value="fuiou" />
|
||||||
@@ -95,11 +97,17 @@
|
|||||||
|
|
||||||
<!-- 微信相关配置 -->
|
<!-- 微信相关配置 -->
|
||||||
<template v-if="form.provider_type === 'wechat'">
|
<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">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="小程序AppID" prop="miniapp_app_id">
|
<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>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
@@ -114,11 +122,17 @@
|
|||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
|
|
||||||
<ElDivider content-position="left">公众号配置</ElDivider>
|
<ElDivider content-position="left">
|
||||||
|
<span class="divider-title">公众号配置</span>
|
||||||
|
</ElDivider>
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="公众号AppID" prop="oa_app_id">
|
<ElFormItem label="公众号AppID" prop="oa_app_id">
|
||||||
<ElInput v-model="form.oa_app_id" placeholder="请输入公众号AppID" />
|
<ElInput
|
||||||
|
v-model="form.oa_app_id"
|
||||||
|
placeholder="请输入公众号AppID"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
@@ -155,19 +169,33 @@
|
|||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
|
<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>
|
</ElFormItem>
|
||||||
|
|
||||||
<ElDivider content-position="left">微信支付配置</ElDivider>
|
<ElDivider content-position="left">
|
||||||
|
<span class="divider-title">微信支付配置</span>
|
||||||
|
</ElDivider>
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="微信商户号" prop="wx_mch_id">
|
<ElFormItem label="微信商户号" prop="wx_mch_id">
|
||||||
<ElInput v-model="form.wx_mch_id" placeholder="请输入微信商户号" />
|
<ElInput
|
||||||
|
v-model="form.wx_mch_id"
|
||||||
|
placeholder="请输入微信商户号"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="证书序列号" prop="wx_serial_no">
|
<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>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
@@ -194,7 +222,11 @@
|
|||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
<ElFormItem label="支付回调地址" prop="wx_notify_url">
|
<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>
|
</ElFormItem>
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
@@ -222,33 +254,207 @@
|
|||||||
|
|
||||||
<!-- 富友支付配置 -->
|
<!-- 富友支付配置 -->
|
||||||
<template v-if="form.provider_type === 'fuiou'">
|
<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">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="富友API地址" prop="fy_api_url">
|
<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>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="富友机构号" prop="fy_ins_cd">
|
<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>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="富友商户号" prop="fy_mchnt_cd">
|
<ElFormItem label="富友商户号" prop="fy_mchnt_cd">
|
||||||
<ElInput v-model="form.fy_mchnt_cd" placeholder="请输入富友商户号" />
|
<ElInput
|
||||||
|
v-model="form.fy_mchnt_cd"
|
||||||
|
placeholder="请输入富友商户号"
|
||||||
|
:disabled="dialogType === 'edit'"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<ElFormItem label="富友终端号" prop="fy_term_id">
|
<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>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
<ElFormItem label="支付回调地址" prop="fy_notify_url">
|
<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>
|
</ElFormItem>
|
||||||
<ElRow :gutter="20">
|
<ElRow :gutter="20">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
@@ -289,7 +495,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from 'vue'
|
import { h, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { WechatConfigService } from '@/api/modules'
|
import { WechatConfigService } from '@/api/modules'
|
||||||
import { ElMessage, ElTag, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
|
import { ElMessage, ElTag, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
|
||||||
@@ -387,51 +593,73 @@
|
|||||||
{ label: '更新时间', prop: 'updated_at' }
|
{ label: '更新时间', prop: 'updated_at' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 动态表单验证规则
|
// URL格式验证规则
|
||||||
const rules = computed<FormRules>(() => {
|
const urlValidator = (rule: any, value: any, callback: any) => {
|
||||||
const baseRules: FormRules = {
|
if (!value) {
|
||||||
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
callback()
|
||||||
provider_type: [{ required: true, message: '请选择支付渠道类型', trigger: 'change' }]
|
return
|
||||||
}
|
}
|
||||||
|
const urlPattern = /^(https?:\/\/)[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?$/
|
||||||
|
if (!urlPattern.test(value)) {
|
||||||
|
callback(new Error('请输入正确的URL格式(http://或https://)'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 只在创建模式下添加必填验证
|
// 自定义验证器 - 编辑模式下允许为空,新增模式下必填
|
||||||
if (dialogType.value === 'add') {
|
const createRequiredValidator = (fieldName: string) => {
|
||||||
if (form.provider_type === 'wechat') {
|
return (rule: any, value: any, callback: any) => {
|
||||||
// 微信直连必填字段
|
if (dialogType.value === 'add' && !value) {
|
||||||
baseRules.wx_mch_id = [{ required: true, message: '请输入微信商户号', trigger: 'blur' }]
|
callback(new Error(`请输入${fieldName}`))
|
||||||
baseRules.wx_api_v3_key = [
|
} else {
|
||||||
{ required: true, message: '请输入微信APIv3密钥', trigger: 'blur' }
|
callback()
|
||||||
]
|
|
||||||
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' }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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: '',
|
name: '',
|
||||||
provider_type: 'wechat' as PaymentProviderType,
|
provider_type: 'wechat' as PaymentProviderType,
|
||||||
description: '',
|
description: '',
|
||||||
@@ -456,9 +684,36 @@
|
|||||||
fy_notify_url: '',
|
fy_notify_url: '',
|
||||||
fy_private_key: '',
|
fy_private_key: '',
|
||||||
fy_public_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[]>([])
|
const configList = ref<WechatConfig[]>([])
|
||||||
|
|
||||||
@@ -583,16 +838,6 @@
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
// 监听支付渠道类型变化,清除表单验证
|
|
||||||
watch(
|
|
||||||
() => form.provider_type,
|
|
||||||
() => {
|
|
||||||
nextTick(() => {
|
|
||||||
formRef.value?.clearValidate()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTableData()
|
getTableData()
|
||||||
})
|
})
|
||||||
@@ -650,45 +895,72 @@
|
|||||||
|
|
||||||
// 显示创建对话框
|
// 显示创建对话框
|
||||||
const showCreateDialog = () => {
|
const showCreateDialog = () => {
|
||||||
|
resetForm()
|
||||||
dialogType.value = 'add'
|
dialogType.value = 'add'
|
||||||
Object.assign(form, { id: 0, ...initialFormState })
|
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
formRef.value?.clearValidate()
|
formRef.value?.clearValidate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理敏感字段值 - 如果是***或[已配置]等特殊值,转换为空字符串
|
||||||
|
const processSensitiveField = (value: string | undefined | null): string => {
|
||||||
|
if (!value || value === '***' || value.includes('[已配置]') || value.includes('[未配置]')) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// 显示编辑对话框
|
// 显示编辑对话框
|
||||||
const showEditDialog = (row: WechatConfig) => {
|
const showEditDialog = async (row: WechatConfig) => {
|
||||||
dialogType.value = 'edit'
|
// 先调用详情接口获取完整数据
|
||||||
Object.assign(form, {
|
try {
|
||||||
id: row.id,
|
const res = await WechatConfigService.getWechatConfigById(row.id)
|
||||||
name: row.name,
|
if (res.code === 0 && res.data) {
|
||||||
provider_type: row.provider_type,
|
const detail = res.data
|
||||||
description: row.description,
|
// 赋值表单数据
|
||||||
miniapp_app_id: row.miniapp_app_id,
|
Object.assign(form, {
|
||||||
miniapp_app_secret: '',
|
id: detail.id,
|
||||||
oa_app_id: row.oa_app_id,
|
name: detail.name,
|
||||||
oa_app_secret: '',
|
provider_type: detail.provider_type,
|
||||||
oa_token: '',
|
description: detail.description || '',
|
||||||
oa_aes_key: '',
|
miniapp_app_id: detail.miniapp_app_id || '',
|
||||||
oa_oauth_redirect_url: row.oa_oauth_redirect_url,
|
miniapp_app_secret: processSensitiveField(detail.miniapp_app_secret),
|
||||||
wx_mch_id: row.wx_mch_id,
|
oa_app_id: detail.oa_app_id || '',
|
||||||
wx_api_v2_key: '',
|
oa_app_secret: processSensitiveField(detail.oa_app_secret),
|
||||||
wx_api_v3_key: '',
|
oa_token: processSensitiveField(detail.oa_token),
|
||||||
wx_notify_url: row.wx_notify_url,
|
oa_aes_key: processSensitiveField(detail.oa_aes_key),
|
||||||
wx_serial_no: row.wx_serial_no,
|
oa_oauth_redirect_url: detail.oa_oauth_redirect_url || '',
|
||||||
wx_cert_content: '',
|
wx_mch_id: detail.wx_mch_id || '',
|
||||||
wx_key_content: '',
|
wx_api_v2_key: processSensitiveField(detail.wx_api_v2_key),
|
||||||
fy_api_url: row.fy_api_url,
|
wx_api_v3_key: processSensitiveField(detail.wx_api_v3_key),
|
||||||
fy_ins_cd: row.fy_ins_cd,
|
wx_notify_url: detail.wx_notify_url || '',
|
||||||
fy_mchnt_cd: row.fy_mchnt_cd,
|
wx_serial_no: detail.wx_serial_no || '',
|
||||||
fy_term_id: row.fy_term_id,
|
wx_cert_content: processSensitiveField(detail.wx_cert_content),
|
||||||
fy_notify_url: row.fy_notify_url,
|
wx_key_content: processSensitiveField(detail.wx_key_content),
|
||||||
fy_private_key: '',
|
fy_api_url: detail.fy_api_url || '',
|
||||||
fy_public_key: ''
|
fy_ins_cd: detail.fy_ins_cd || '',
|
||||||
})
|
fy_mchnt_cd: detail.fy_mchnt_cd || '',
|
||||||
dialogVisible.value = true
|
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(() => {
|
nextTick(() => {
|
||||||
formRef.value?.clearValidate()
|
formRef.value?.clearValidate()
|
||||||
})
|
})
|
||||||
@@ -696,8 +968,8 @@
|
|||||||
|
|
||||||
// 对话框关闭后的清理
|
// 对话框关闭后的清理
|
||||||
const handleDialogClosed = () => {
|
const handleDialogClosed = () => {
|
||||||
formRef.value?.resetFields()
|
// 不调用resetFields,因为会把数据也重置了
|
||||||
Object.assign(form, { id: 0, ...initialFormState })
|
// 只需要什么都不做,下次打开时会重新赋值
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
@@ -772,7 +1044,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
formRef.value?.resetFields()
|
|
||||||
await getTableData()
|
await getTableData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -898,4 +1169,46 @@
|
|||||||
:deep(.el-table__row.table-row-with-context-menu) {
|
:deep(.el-table__row.table-row-with-context-menu) {
|
||||||
cursor: pointer;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user