refactor(ops): 优化任务心跳和组件刷新机制
后端改动: - 添加 ops_job_heartbeats.last_result 字段记录任务执行结果 - 优化告警评估器统计信息(规则数/事件数/邮件数) - 统一各定时任务的心跳记录格式 前端改动: - 重构 OpsConcurrencyCard 使用父组件统一控制刷新节奏 - 移除独立的 5 秒刷新定时器,改用 refreshToken 机制 - 修复 TypeScript 类型错误
This commit is contained in:
@@ -293,6 +293,7 @@ export interface OpsJobHeartbeat {
|
||||
last_error_at?: string | null
|
||||
last_error?: string | null
|
||||
last_duration_ms?: number | null
|
||||
last_result?: string | null
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ const handleScroll = () => {
|
||||
menu.show = false
|
||||
}
|
||||
|
||||
onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) }; window.addEventListener('scroll', handleScroll, true) })
|
||||
onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) } window.addEventListener('scroll', handleScroll, true) })
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<!-- Row: Concurrency + Throughput -->
|
||||
<div v-if="opsEnabled && !(loading && !hasLoadedOnce)" class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-1 min-h-[360px]">
|
||||
<OpsConcurrencyCard :platform-filter="platform" :group-id-filter="groupId" />
|
||||
<OpsConcurrencyCard :platform-filter="platform" :group-id-filter="groupId" :refresh-token="dashboardRefreshToken" />
|
||||
</div>
|
||||
<div class="lg:col-span-2 min-h-[360px]">
|
||||
<OpsThroughputTrendChart
|
||||
@@ -352,6 +352,9 @@ const autoRefreshEnabled = ref(false)
|
||||
const autoRefreshIntervalMs = ref(30000) // default 30 seconds
|
||||
const autoRefreshCountdown = ref(0)
|
||||
|
||||
// Used to trigger child component refreshes in a single shared cadence.
|
||||
const dashboardRefreshToken = ref(0)
|
||||
|
||||
// Auto refresh timer
|
||||
const { pause: pauseAutoRefresh, resume: resumeAutoRefresh } = useIntervalFn(
|
||||
() => {
|
||||
@@ -597,7 +600,12 @@ async function fetchData() {
|
||||
refreshErrorDistributionWithCancel(fetchSeq, dashboardFetchController.signal)
|
||||
])
|
||||
if (fetchSeq !== dashboardFetchSeq) return
|
||||
|
||||
lastUpdated.value = new Date()
|
||||
|
||||
// Trigger child component refreshes using the same cadence as the header.
|
||||
dashboardRefreshToken.value += 1
|
||||
|
||||
// Reset auto refresh countdown after successful fetch
|
||||
if (autoRefreshEnabled.value) {
|
||||
autoRefreshCountdown.value = Math.floor(autoRefreshIntervalMs.value / 1000)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { opsAPI, type OpsAccountAvailabilityStatsResponse, type OpsConcurrencyStatsResponse } from '@/api/admin/ops'
|
||||
|
||||
interface Props {
|
||||
platformFilter?: string
|
||||
groupIdFilter?: number | null
|
||||
refreshToken: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -233,15 +233,13 @@ async function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 定期刷新(5秒)
|
||||
const { pause: pauseRefresh, resume: resumeRefresh } = useIntervalFn(
|
||||
// 刷新节奏由父组件统一控制(OpsDashboard Header 的刷新状态/倒计时)
|
||||
watch(
|
||||
() => props.refreshToken,
|
||||
() => {
|
||||
if (realtimeEnabled.value) {
|
||||
loadData()
|
||||
}
|
||||
},
|
||||
5000,
|
||||
{ immediate: false }
|
||||
if (!realtimeEnabled.value) return
|
||||
loadData()
|
||||
}
|
||||
)
|
||||
|
||||
function getLoadBarClass(loadPct: number): string {
|
||||
@@ -271,23 +269,15 @@ function formatDuration(seconds: number): string {
|
||||
return `${hours}h`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
resumeRefresh()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pauseRefresh()
|
||||
})
|
||||
|
||||
watch(realtimeEnabled, async (enabled) => {
|
||||
if (!enabled) {
|
||||
pauseRefresh()
|
||||
} else {
|
||||
resumeRefresh()
|
||||
await loadData()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => realtimeEnabled.value,
|
||||
async (enabled) => {
|
||||
if (enabled) {
|
||||
await loadData()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import HelpTooltip from '@/components/common/HelpTooltip.vue'
|
||||
@@ -315,31 +314,33 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const { pause: pauseRealtimeTrafficRefresh, resume: resumeRealtimeTrafficRefresh } = useIntervalFn(
|
||||
() => {
|
||||
loadRealtimeTrafficSummary()
|
||||
},
|
||||
5000,
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => adminSettingsStore.opsRealtimeMonitoringEnabled,
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
resumeRealtimeTrafficRefresh()
|
||||
} else {
|
||||
pauseRealtimeTrafficRefresh()
|
||||
if (!enabled) {
|
||||
// Keep UI stable when realtime monitoring is turned off.
|
||||
realtimeTrafficSummary.value = makeZeroRealtimeTrafficSummary()
|
||||
} else {
|
||||
loadRealtimeTrafficSummary()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
pauseRealtimeTrafficRefresh()
|
||||
})
|
||||
// Realtime traffic refresh follows the parent (OpsDashboard) refresh cadence.
|
||||
watch(
|
||||
() => [props.autoRefreshEnabled, props.autoRefreshCountdown, props.loading] as const,
|
||||
([enabled, countdown, loading]) => {
|
||||
if (!enabled) return
|
||||
if (loading) return
|
||||
// Treat countdown reset (or reaching 0) as a refresh boundary.
|
||||
if (countdown === 0) {
|
||||
loadRealtimeTrafficSummary()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// no-op: parent controls refresh cadence
|
||||
|
||||
const displayRealTimeQps = computed(() => {
|
||||
const v = realtimeTrafficSummary.value?.qps?.current
|
||||
@@ -1442,7 +1443,7 @@ function handleToolbarRefresh() {
|
||||
<!-- MEM -->
|
||||
<div class="rounded-xl bg-gray-50 p-3 dark:bg-dark-900">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wider text-gray-400">{{ t('admin.ops.mem') }}</div>
|
||||
<div class="text-[10px] font-bold uppercase tracking-wider text-gray-400">{{ t('admin.ops.memory') }}</div>
|
||||
<HelpTooltip v-if="!props.fullscreen" :content="t('admin.ops.tooltips.memory')" />
|
||||
</div>
|
||||
<div class="mt-1 text-lg font-black" :class="memPercentClass">
|
||||
@@ -1545,7 +1546,10 @@ function handleToolbarRefresh() {
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="truncate text-sm font-semibold text-gray-900 dark:text-white">{{ hb.job_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ formatTimeShort(hb.updated_at) }}</div>
|
||||
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span v-if="hb.last_duration_ms != null" class="font-mono">{{ hb.last_duration_ms }}ms</span>
|
||||
<span>{{ formatTimeShort(hb.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 gap-2 text-xs text-gray-600 dark:text-gray-300 sm:grid-cols-2">
|
||||
@@ -1555,6 +1559,9 @@ function handleToolbarRefresh() {
|
||||
<div>
|
||||
{{ t('admin.ops.lastError') }} <span class="font-mono">{{ formatTimeShort(hb.last_error_at) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ t('admin.ops.result') }} <span class="font-mono">{{ hb.last_result || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user