mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 05:30:44 +08:00
Reference check-cx UI: INTELLIGENCE MONITOR hero + 3-column card grid with 60-point timeline bars. Backend: - Add PrimaryPingLatencyMs + Timeline[60] to UserMonitorView - ListRecentHistoryForMonitors: batch CTE + ROW_NUMBER() window query - indexLatestByModel / indexAvailabilityByModel helpers Frontend: - 7 new components: ProviderIcon, MonitorMetricPair, MonitorAvailabilityRow, MonitorTimeline, MonitorHero, MonitorCard, MonitorCardGrid - ChannelStatusView 381→~180 lines (delegated to subcomponents) - AbortController reload concurrency protection - HSL 0-120° availability color mapping - Replace emoji with Icon component (bolt / globe) - i18n: monitorCommon.* shared namespace, channelStatus.hero.* Bump VERSION to 0.1.114.24
114 lines
3.2 KiB
Vue
114 lines
3.2 KiB
Vue
<template>
|
|
<div class="mt-4 pt-3 border-t border-gray-100 dark:border-dark-700/60">
|
|
<div
|
|
class="flex justify-between text-[10px] font-semibold uppercase tracking-widest text-gray-400 mb-2"
|
|
>
|
|
<span>{{ t('monitorCommon.history60pts', { n: length }) }}</span>
|
|
<span class="tabular-nums">{{ t('monitorCommon.nextUpdateIn', { n: countdownSeconds }) }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="maintenance"
|
|
class="flex h-5 w-full items-center justify-center rounded border border-dashed border-gray-300 dark:border-dark-600 text-[10px] uppercase tracking-widest text-gray-400"
|
|
>
|
|
{{ t('monitorCommon.maintenancePaused') }}
|
|
</div>
|
|
<div v-else class="flex items-end gap-[2px] h-5 w-full">
|
|
<div
|
|
v-for="(bar, idx) in displayBars"
|
|
:key="idx"
|
|
class="flex-1 min-w-[3px] rounded-sm"
|
|
:class="bar.colorClass"
|
|
:style="{ height: bar.heightPct + '%' }"
|
|
:title="bar.title"
|
|
></div>
|
|
</div>
|
|
|
|
<div
|
|
class="mt-1 flex justify-between text-[9px] uppercase tracking-widest text-gray-400"
|
|
>
|
|
<span>{{ t('monitorCommon.past') }}</span>
|
|
<span>{{ t('monitorCommon.now') }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { MonitorTimelinePoint } from '@/api/channelMonitor'
|
|
import { useChannelMonitorFormat } from '@/composables/useChannelMonitorFormat'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
buckets?: MonitorTimelinePoint[]
|
|
countdownSeconds: number
|
|
length?: number
|
|
maintenance?: boolean
|
|
}>(), {
|
|
buckets: () => [],
|
|
length: 60,
|
|
maintenance: false,
|
|
})
|
|
|
|
const { t } = useI18n()
|
|
const { statusLabel, formatLatency, formatRelativeTime } = useChannelMonitorFormat()
|
|
|
|
interface Bar {
|
|
colorClass: string
|
|
heightPct: number
|
|
title: string
|
|
}
|
|
|
|
const STATUS_HEIGHT: Record<string, number> = {
|
|
operational: 100,
|
|
degraded: 70,
|
|
failed: 55,
|
|
error: 35,
|
|
empty: 20,
|
|
}
|
|
|
|
const STATUS_COLOR: Record<string, string> = {
|
|
operational: 'bg-emerald-500',
|
|
degraded: 'bg-amber-500',
|
|
failed: 'bg-red-500',
|
|
error: 'bg-gray-400 dark:bg-dark-500',
|
|
empty: 'bg-gray-300 dark:bg-dark-600',
|
|
}
|
|
|
|
const displayBars = computed<Bar[]>(() => {
|
|
// Real points come newest-first; convert to oldest-first so the rightmost
|
|
// bar represents "now". Pad the left with empty placeholders to keep the
|
|
// bar count stable at `length`.
|
|
const real = [...(props.buckets ?? [])]
|
|
.slice(0, props.length)
|
|
.reverse()
|
|
|
|
const padCount = Math.max(0, props.length - real.length)
|
|
const bars: Bar[] = []
|
|
|
|
for (let i = 0; i < padCount; i += 1) {
|
|
bars.push({
|
|
colorClass: STATUS_COLOR.empty,
|
|
heightPct: STATUS_HEIGHT.empty,
|
|
title: '',
|
|
})
|
|
}
|
|
|
|
for (const point of real) {
|
|
const status = point.status as keyof typeof STATUS_HEIGHT
|
|
const colorClass = STATUS_COLOR[status] ?? STATUS_COLOR.empty
|
|
const heightPct = STATUS_HEIGHT[status] ?? STATUS_HEIGHT.empty
|
|
const latency = formatLatency(point.latency_ms)
|
|
const relative = formatRelativeTime(point.checked_at)
|
|
const label = statusLabel(point.status)
|
|
bars.push({
|
|
colorClass,
|
|
heightPct,
|
|
title: `${relative} · ${label} · ${latency}ms`,
|
|
})
|
|
}
|
|
|
|
return bars
|
|
})
|
|
</script>
|