简单的定时任务--ok

This commit is contained in:
yml2213
2026-05-14 17:59:45 +08:00
parent b6c493d5e7
commit f7f3289d04
19 changed files with 954 additions and 5 deletions
@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { AdminNotificationTestResult } from '@/types/admin'
import type { EditableNotificationRecipient } from '@/composables/admin/platform-shops/types'
import { computed } from 'vue'
import type { AdminNotificationTestResult, AdminScheduledJobRuntimeState } from '@/types/admin'
import type { EditableNotificationRecipient, EditableScheduledJob } from '@/composables/admin/platform-shops/types'
import { formatAdminDateTime } from '@/utils/admin-time'
type Props = {
notificationFilePath: string
@@ -17,20 +19,103 @@ type Props = {
notificationTesting: boolean
notificationResultError: string
notificationTestResult: AdminNotificationTestResult | null
scheduledJobsFilePath: string
scheduledJobsForm: {
enabled: boolean
}
scheduledJobs: EditableScheduledJob[]
scheduledJobRuntime: AdminScheduledJobRuntimeState[]
scheduledJobsSaving: boolean
scheduledJobRunningId: string
notificationStats: {
configuredRecipientCount: number
enabledRecipientCount: number
barkReady: boolean
lastSuccessCount: number
lastFailedCount: number
scheduledJobEnabledCount: number
}
addNotificationRecipient: () => void
removeNotificationRecipient: (id: string) => void
handleNotificationSaveConfig: () => void | Promise<void>
handleNotificationTest: () => void | Promise<void>
handleScheduledJobsSaveConfig: () => void | Promise<void>
handleScheduledJobRunNow: (jobId: string) => void | Promise<void>
getScheduledJobRuntime: (jobId: string) => AdminScheduledJobRuntimeState | null
}
defineProps<Props>()
const timeUnitOptions = [
{ label: '秒', value: 1 },
{ label: '分钟', value: 60 },
{ label: '小时', value: 3600 },
]
function formatJobType(type: string) {
if (type === 'cloudtentacles_health') {
return 'cloudtentacles 健康检查'
}
return type || '-'
}
function resolveTimeValue(seconds: number) {
const normalized = Math.max(0, Number(seconds || 0))
if (normalized >= 3600 && normalized % 3600 === 0) {
return { amount: normalized / 3600, unit: 3600 }
}
if (normalized >= 60 && normalized % 60 === 0) {
return { amount: normalized / 60, unit: 60 }
}
return { amount: normalized || 1, unit: 1 }
}
function formatDuration(seconds: number) {
const value = resolveTimeValue(seconds)
const unit = timeUnitOptions.find((item) => item.value === value.unit)?.label || '秒'
return `${value.amount} ${unit}`
}
function bindDuration(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
const amountKey = `${key}Amount` as keyof EditableScheduledJob
const unitKey = `${key}Unit` as keyof EditableScheduledJob
if (!Number(job[amountKey])) {
const resolved = resolveTimeValue(Number(job[key] || 0))
;(job[amountKey] as number) = resolved.amount
;(job[unitKey] as number) = resolved.unit
}
return computed({
get: () => Number(job[amountKey] || 1),
set: (value: number) => {
;(job[amountKey] as number) = Math.max(1, Number(value || 1))
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
},
})
}
function bindDurationUnit(job: EditableScheduledJob, key: 'intervalSeconds' | 'cooldownSeconds') {
const amountKey = `${key}Amount` as keyof EditableScheduledJob
const unitKey = `${key}Unit` as keyof EditableScheduledJob
if (!Number(job[unitKey])) {
const resolved = resolveTimeValue(Number(job[key] || 0))
;(job[amountKey] as number) = resolved.amount
;(job[unitKey] as number) = resolved.unit
}
return computed({
get: () => Number(job[unitKey] || 1),
set: (value: number) => {
;(job[unitKey] as number) = Math.max(1, Number(value || 1))
job[key] = Number(job[amountKey] || 1) * Number(job[unitKey] || 1)
},
})
}
</script>
<template>
@@ -43,6 +128,10 @@ defineProps<Props>()
<span class="meta-label">配置文件</span>
<span>{{ notificationFilePath || '-' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">监控任务</span>
<span>{{ scheduledJobsFilePath || '-' }}</span>
</div>
</section>
<section class="table-card">
@@ -108,6 +197,99 @@ defineProps<Props>()
</table>
</section>
<section class="table-card">
<div class="section-title-row">
<div>
<h3>监控任务</h3>
<p>间隔表示多久检查一次冷却表示同一个异常在这段时间内最多通知一次</p>
</div>
<div class="section-actions">
<el-button :loading="scheduledJobsSaving" round type="primary" @click="handleScheduledJobsSaveConfig">保存任务</el-button>
</div>
</div>
<div class="form-grid">
<label class="checkbox-line">
<input v-model="scheduledJobsForm.enabled" class="checkbox-box" type="checkbox" />
<span>启用监控任务系统</span>
</label>
</div>
<table class="data-table">
<thead>
<tr>
<th>任务</th>
<th>参数</th>
<th>最近状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="job in scheduledJobs" :key="job.id">
<td>
<div class="cell-stack">
<strong>{{ formatJobType(job.type) }}</strong>
<label class="checkbox-line">
<input v-model="job.enabled" class="checkbox-box" type="checkbox" />
<span>{{ job.enabled ? '启用' : '停用' }}</span>
</label>
</div>
</td>
<td>
<div class="form-grid">
<label class="field-block">
<span>检查间隔</span>
<div class="time-input-row">
<input v-model.number="bindDuration(job, 'intervalSeconds').value" class="text-input" type="number" min="1" />
<select v-model.number="bindDurationUnit(job, 'intervalSeconds').value" class="text-input time-unit-select">
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">{{ unit.label }}</option>
</select>
</div>
<span class="cell-subtle"> {{ formatDuration(job.intervalSeconds) }} 检查一次</span>
</label>
<label class="field-block">
<span>通知冷却</span>
<div class="time-input-row">
<input v-model.number="bindDuration(job, 'cooldownSeconds').value" class="text-input" type="number" min="1" />
<select v-model.number="bindDurationUnit(job, 'cooldownSeconds').value" class="text-input time-unit-select">
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">{{ unit.label }}</option>
</select>
</div>
<span class="cell-subtle">同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span>
</label>
<label class="field-block">
<span>余额阈值</span>
<input v-model.number="job.assetThreshold" class="text-input" type="number" min="0" />
<span class="cell-subtle">低于该值时通知</span>
</label>
</div>
</td>
<td>
<div class="cell-stack">
<strong>{{ getScheduledJobRuntime(job.id)?.lastStatus || 'pending' }}</strong>
<span class="cell-subtle">{{ getScheduledJobRuntime(job.id)?.lastMessage || '-' }}</span>
<span class="cell-subtle">余额{{ getScheduledJobRuntime(job.id)?.lastAsset ?? '-' }}</span>
<span class="cell-subtle">上次{{ formatAdminDateTime(getScheduledJobRuntime(job.id)?.lastRunAt || '') }}</span>
<span class="cell-subtle">下次{{ formatAdminDateTime(getScheduledJobRuntime(job.id)?.nextRunAt || '') }}</span>
</div>
</td>
<td>
<el-button
:loading="scheduledJobRunningId === job.id"
round
@click="handleScheduledJobRunNow(job.id)"
>
立即检查
</el-button>
</td>
</tr>
<tr v-if="scheduledJobs.length === 0">
<td colspan="4" class="empty-inline">暂无可配置的监控任务</td>
</tr>
</tbody>
</table>
</section>
<section class="table-card">
<div class="section-title-row">
<div>