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
82 lines
2.6 KiB
Vue
82 lines
2.6 KiB
Vue
<template>
|
|
<div>
|
|
<div
|
|
v-if="loading && items.length === 0"
|
|
class="grid gap-5 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
|
>
|
|
<div
|
|
v-for="i in 6"
|
|
:key="i"
|
|
class="p-5 rounded-2xl min-h-[280px] bg-white/70 dark:bg-dark-800/60 border border-gray-200/80 dark:border-dark-700/70 animate-pulse"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<div class="w-9 h-9 rounded-xl bg-gray-200 dark:bg-dark-700"></div>
|
|
<div class="flex-1 space-y-2">
|
|
<div class="h-4 w-2/3 rounded bg-gray-200 dark:bg-dark-700"></div>
|
|
<div class="h-3 w-1/2 rounded bg-gray-200 dark:bg-dark-700"></div>
|
|
</div>
|
|
<div class="h-6 w-16 rounded-full bg-gray-200 dark:bg-dark-700"></div>
|
|
</div>
|
|
<div class="mt-5 grid grid-cols-2 gap-2">
|
|
<div class="h-16 rounded-xl bg-gray-100 dark:bg-dark-900/40"></div>
|
|
<div class="h-16 rounded-xl bg-gray-100 dark:bg-dark-900/40"></div>
|
|
</div>
|
|
<div class="mt-6 h-5 w-full rounded bg-gray-100 dark:bg-dark-900/40"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<EmptyState
|
|
v-else-if="items.length === 0"
|
|
:title="t('channelStatus.empty.title')"
|
|
:description="t('channelStatus.empty.description')"
|
|
/>
|
|
|
|
<div
|
|
v-else
|
|
class="grid gap-5 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
|
>
|
|
<MonitorCard
|
|
v-for="item in items"
|
|
:key="item.id"
|
|
:item="item"
|
|
:window="window"
|
|
:availability-value="resolveAvailability(item)"
|
|
:countdown-seconds="countdownSeconds"
|
|
@click="emit('cardClick', item)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { UserMonitorView, UserMonitorDetail } from '@/api/channelMonitor'
|
|
import EmptyState from '@/components/common/EmptyState.vue'
|
|
import MonitorCard from './MonitorCard.vue'
|
|
|
|
const props = defineProps<{
|
|
items: UserMonitorView[]
|
|
window: '7d' | '15d' | '30d'
|
|
countdownSeconds: number
|
|
loading: boolean
|
|
detailCache: Record<number, UserMonitorDetail>
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'cardClick', item: UserMonitorView): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
function resolveAvailability(item: UserMonitorView): number | null {
|
|
if (props.window === '7d') {
|
|
return item.availability_7d ?? null
|
|
}
|
|
const detail = props.detailCache[item.id]
|
|
if (!detail) return null
|
|
const primary = detail.models.find(m => m.model === item.primary_model)
|
|
if (!primary) return null
|
|
return props.window === '15d' ? primary.availability_15d ?? null : primary.availability_30d ?? null
|
|
}
|
|
</script>
|