You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
116 lines
5.0 KiB
116 lines
5.0 KiB
<template>
|
|
<!-- 仪表盘顶部第三方服务异常告警条。没有异常时整块不渲染,不占首屏。 -->
|
|
<div v-if="errorCount + warnCount > 0" class="alert-banner" :class="errorCount > 0 ? 'lv-error' : 'lv-warn'">
|
|
<div class="ab-head">
|
|
<span class="ab-ico">{{ errorCount > 0 ? '🚨' : '⚠️' }}</span>
|
|
<div class="ab-title">
|
|
<strong>
|
|
{{ errorCount > 0 ? `${errorCount} 项第三方服务不可用` : `${warnCount} 项第三方服务存在隐患` }}
|
|
</strong>
|
|
<span v-if="errorCount > 0 && warnCount > 0" class="ab-sub">,另有 {{ warnCount }} 项隐患</span>
|
|
<span class="ab-sub"> · 最近巡检 {{ lastRunText }}</span>
|
|
</div>
|
|
<button class="ab-btn" @click="expanded = !expanded">{{ expanded ? '收起' : '展开详情' }}</button>
|
|
<NuxtLink to="/svcinspect" class="ab-btn ab-btn-primary">前往巡检</NuxtLink>
|
|
</div>
|
|
|
|
<div v-if="expanded" class="ab-list">
|
|
<div v-for="a in alerts" :key="`${a.app_name}/${a.svc_id}/${a.region}`" class="ab-item">
|
|
<span class="ab-dot" :class="a.status === 'error' ? 'd-error' : 'd-warn'"></span>
|
|
<span class="ab-name">{{ a.name || a.svc_id }}</span>
|
|
<span v-if="a.provider" class="ab-chip">{{ a.provider }}</span>
|
|
<span class="ab-chip ab-chip-scope">{{ scopeText(a) }}</span>
|
|
<span class="ab-msg">{{ reasonOf(a) }}</span>
|
|
<span v-if="a.fail_streak > 1" class="ab-streak">连续失败 {{ a.fail_streak }} 次</span>
|
|
</div>
|
|
<div v-if="truncated" class="ab-more">
|
|
仅显示前 {{ alerts.length }} 条,其余请到<NuxtLink to="/svcinspect" class="ab-link">服务巡检</NuxtLink>查看
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { REGION_LABEL, healthReason, healthLastRunText, type SvcHealth } from '~/utils/svchealth'
|
|
|
|
const { webApi } = useApi()
|
|
|
|
const alerts = ref<SvcHealth[]>([])
|
|
const errorCount = ref(0)
|
|
const warnCount = ref(0)
|
|
const lastRun = ref(0)
|
|
const expanded = ref(false)
|
|
const truncated = ref(false)
|
|
|
|
const LIMIT = 12
|
|
|
|
const lastRunText = computed(() => healthLastRunText(lastRun.value))
|
|
|
|
function scopeText(a: SvcHealth): string {
|
|
const app = a.app_name ? a.app_name : '全局'
|
|
return a.region ? `${app} · ${REGION_LABEL[a.region] || '区域' + a.region}` : app
|
|
}
|
|
|
|
function reasonOf(a: SvcHealth): string {
|
|
return healthReason(a)
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
const res = await webApi<{
|
|
alerts: SvcHealth[]
|
|
error_count: number
|
|
warn_count: number
|
|
last_run: number
|
|
}>('getsvcalerts', { limit: LIMIT })
|
|
alerts.value = res.alerts || []
|
|
errorCount.value = res.error_count || 0
|
|
warnCount.value = res.warn_count || 0
|
|
lastRun.value = res.last_run || 0
|
|
truncated.value = errorCount.value + warnCount.value > alerts.value.length
|
|
// 有明确不可用的服务时默认展开——这类问题需要马上看见是哪几个,而不是再点一次。
|
|
expanded.value = errorCount.value > 0
|
|
} catch {
|
|
// 巡检数据拿不到不应该拖垮整个仪表盘,静默降级为「无告警」。
|
|
alerts.value = []
|
|
errorCount.value = 0
|
|
warnCount.value = 0
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
defineExpose({ load })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.alert-banner { border-radius:14px; padding:12px 16px; margin-bottom:18px; border:1px solid; }
|
|
.lv-error { background:linear-gradient(180deg,#fff1f2 0%,#fff7f7 100%); border-color:#fecdd3; }
|
|
.lv-warn { background:linear-gradient(180deg,#fffbeb 0%,#fffdf5 100%); border-color:#fde68a; }
|
|
|
|
.ab-head { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
.ab-ico { font-size:17px; line-height:1; }
|
|
.ab-title { font-size:13px; color:#334155; flex:1; min-width:200px; }
|
|
.lv-error .ab-title strong { color:#be123c; }
|
|
.lv-warn .ab-title strong { color:#b45309; }
|
|
.ab-sub { color:#94a3b8; font-size:12px; }
|
|
|
|
.ab-btn { font-size:12px; padding:4px 11px; border-radius:8px; border:1px solid #e2e8f0; background:#fff; color:#475569; white-space:nowrap; }
|
|
.ab-btn:hover { background:#f8fafc; }
|
|
.ab-btn-primary { background:#0f172a; border-color:#0f172a; color:#fff; }
|
|
.ab-btn-primary:hover { background:#1e293b; }
|
|
|
|
.ab-list { margin-top:10px; padding-top:10px; border-top:1px dashed rgba(148,163,184,.4); display:flex; flex-direction:column; gap:6px; }
|
|
.ab-item { display:flex; align-items:center; gap:8px; font-size:12.5px; color:#475569; flex-wrap:wrap; }
|
|
.ab-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
|
|
.d-error { background:#f43f5e; }
|
|
.d-warn { background:#f59e0b; }
|
|
.ab-name { font-weight:600; color:#1e293b; }
|
|
.ab-chip { font-size:11px; padding:1px 7px; border-radius:999px; background:#f1f5f9; color:#64748b; }
|
|
.ab-chip-scope { background:#e0e7ff; color:#4338ca; }
|
|
.ab-msg { color:#64748b; flex:1; min-width:160px; }
|
|
.ab-streak { font-size:11px; color:#be123c; font-weight:600; }
|
|
.ab-more { font-size:12px; color:#94a3b8; padding-top:2px; }
|
|
.ab-link { color:#4338ca; text-decoration:underline; margin:0 2px; }
|
|
</style>
|
|
|