- AppSidebar 三处菜单项(管理端渠道监控、用户端/个人页渠道状态)按 channel_monitor_enabled 条件展开,关闭时隐藏 - ChannelStatusView setInterval 随开关启停:关闭 clearInterval, 开启/未知态自动启动,避免禁用功能后仍在轮询 - MonitorFormDialog provider Select 改为 3 色单选按钮 (openai=emerald / anthropic=orange / gemini=sky),i18n 文案 供应商 → 平台 / Provider → Platform - MonitorKeyPickerDialog 按钮列表改为 name/key/group 三列表格 + 搜索框,按 key.group.platform === provider 过滤,避免跨平台误选 - form.provider 变化时清空 api_key,修复切换平台仍保留旧 key 的 错配 bug - providerPickerClass 抽取到 useChannelMonitorFormat composable, 统一 emerald/orange/sky 颜色语义,消除硬编码 Tailwind class 重复 - maskApiKey 工具函数统一(utils/maskApiKey.ts),KeysView 与 MonitorKeyPickerDialog 共用 slice(0,6)...slice(-4) 策略 - bump version to 0.1.114.27
173 lines
6.6 KiB
TypeScript
173 lines
6.6 KiB
TypeScript
/**
|
|
* Shared formatting helpers for channel monitor views (admin + user).
|
|
*
|
|
* Centralises:
|
|
* - status / provider label + badge class lookups
|
|
* - latency / availability / percent number formatting
|
|
* - dashboard-style helpers (HSL for availability, provider gradient, relative time)
|
|
*
|
|
* i18n keys live under `monitorCommon.*` so admin and user views share the
|
|
* same translation source.
|
|
*/
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { MonitorStatus, Provider } from '@/api/admin/channelMonitor'
|
|
import {
|
|
PROVIDER_OPENAI,
|
|
PROVIDER_ANTHROPIC,
|
|
PROVIDER_GEMINI,
|
|
STATUS_OPERATIONAL,
|
|
STATUS_DEGRADED,
|
|
STATUS_FAILED,
|
|
STATUS_ERROR,
|
|
} from '@/constants/channelMonitor'
|
|
|
|
const NEUTRAL_BADGE = 'bg-gray-100 text-gray-800 dark:bg-dark-700 dark:text-gray-300'
|
|
|
|
/** Availability HSL hue multiplier: 0%=red(0) / 50%=yellow(60) / 100%=green(120). */
|
|
const HSL_HUE_PER_PERCENT = 1.2
|
|
const HSL_SATURATION = 72
|
|
const HSL_LIGHTNESS = 42
|
|
|
|
export interface AvailabilityRow {
|
|
primary_status: MonitorStatus | ''
|
|
availability_7d: number | null | undefined
|
|
}
|
|
|
|
export function useChannelMonitorFormat() {
|
|
const { t } = useI18n()
|
|
|
|
function statusLabel(s: MonitorStatus | ''): string {
|
|
if (!s) return t('monitorCommon.status.unknown')
|
|
return t(`monitorCommon.status.${s}`)
|
|
}
|
|
|
|
function statusBadgeClass(s: MonitorStatus | ''): string {
|
|
switch (s) {
|
|
case STATUS_OPERATIONAL:
|
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300'
|
|
case STATUS_DEGRADED:
|
|
return 'bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300'
|
|
case STATUS_FAILED:
|
|
return 'bg-red-100 text-red-700 dark:bg-red-500/15 dark:text-red-300'
|
|
case STATUS_ERROR:
|
|
default:
|
|
return NEUTRAL_BADGE
|
|
}
|
|
}
|
|
|
|
function providerLabel(p: Provider | string): string {
|
|
if (p === PROVIDER_OPENAI || p === PROVIDER_ANTHROPIC || p === PROVIDER_GEMINI) {
|
|
return t(`monitorCommon.providers.${p}`)
|
|
}
|
|
return p || '-'
|
|
}
|
|
|
|
function providerBadgeClass(p: Provider | string): string {
|
|
switch (p) {
|
|
case PROVIDER_OPENAI:
|
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300'
|
|
case PROVIDER_ANTHROPIC:
|
|
return 'bg-orange-100 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300'
|
|
case PROVIDER_GEMINI:
|
|
return 'bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300'
|
|
default:
|
|
return NEUTRAL_BADGE
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tailwind class for a provider radio-button-style picker (active/inactive state).
|
|
* Reuses the same emerald/orange/sky palette as providerBadgeClass to keep
|
|
* visual semantics consistent across badges and pickers.
|
|
*/
|
|
function providerPickerClass(p: Provider | string, active: boolean): string {
|
|
switch (p) {
|
|
case PROVIDER_OPENAI:
|
|
return active
|
|
? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-400'
|
|
: 'border-gray-200 bg-white text-gray-600 hover:border-emerald-300 hover:text-emerald-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-emerald-500/50'
|
|
case PROVIDER_ANTHROPIC:
|
|
return active
|
|
? 'border-orange-500 bg-orange-50 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300 dark:border-orange-400'
|
|
: 'border-gray-200 bg-white text-gray-600 hover:border-orange-300 hover:text-orange-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-orange-500/50'
|
|
case PROVIDER_GEMINI:
|
|
return active
|
|
? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-400'
|
|
: 'border-gray-200 bg-white text-gray-600 hover:border-sky-300 hover:text-sky-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-sky-500/50'
|
|
default:
|
|
return active
|
|
? 'border-gray-400 bg-gray-50 text-gray-700 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-200'
|
|
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400'
|
|
}
|
|
}
|
|
|
|
function formatLatency(ms: number | null | undefined): string {
|
|
if (ms == null) return t('monitorCommon.latencyEmpty')
|
|
return String(Math.round(ms))
|
|
}
|
|
|
|
function formatPercent(v: number | null | undefined): string {
|
|
if (v == null || Number.isNaN(v)) return '-'
|
|
return `${v.toFixed(2)}%`
|
|
}
|
|
|
|
function formatAvailability(row: AvailabilityRow): string {
|
|
if (!row.primary_status) return '-'
|
|
return formatPercent(row.availability_7d)
|
|
}
|
|
|
|
function formatRelativeTime(iso: string | null | undefined): string {
|
|
if (!iso) return t('monitorCommon.latencyEmpty')
|
|
const ts = Date.parse(iso)
|
|
if (Number.isNaN(ts)) return t('monitorCommon.latencyEmpty')
|
|
const diffSec = Math.max(0, Math.floor((Date.now() - ts) / 1000))
|
|
if (diffSec < 60) return t('monitorCommon.relativeSecondsAgo', { n: diffSec })
|
|
const diffMin = Math.floor(diffSec / 60)
|
|
if (diffMin < 60) return t('monitorCommon.relativeMinutesAgo', { n: diffMin })
|
|
const diffHour = Math.floor(diffMin / 60)
|
|
if (diffHour < 24) return t('monitorCommon.relativeHoursAgo', { n: diffHour })
|
|
const diffDay = Math.floor(diffHour / 24)
|
|
return t('monitorCommon.relativeDaysAgo', { n: diffDay })
|
|
}
|
|
|
|
return {
|
|
statusLabel,
|
|
statusBadgeClass,
|
|
providerLabel,
|
|
providerBadgeClass,
|
|
providerPickerClass,
|
|
formatLatency,
|
|
formatPercent,
|
|
formatAvailability,
|
|
formatRelativeTime,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map availability percent to an HSL colour (red -> yellow -> green).
|
|
* Returns undefined for null/NaN so callers can fall back to a neutral colour.
|
|
*/
|
|
export function hslForPct(pct: number | null | undefined): string | undefined {
|
|
if (pct === null || pct === undefined || Number.isNaN(pct)) return undefined
|
|
const clamped = Math.max(0, Math.min(100, pct))
|
|
const hue = clamped * HSL_HUE_PER_PERCENT
|
|
return `hsl(${hue} ${HSL_SATURATION}% ${HSL_LIGHTNESS}%)`
|
|
}
|
|
|
|
/**
|
|
* Tailwind gradient class for the provider icon tile background.
|
|
*/
|
|
export function providerGradient(provider: string): string {
|
|
switch (provider) {
|
|
case PROVIDER_OPENAI:
|
|
return 'bg-gradient-to-br from-emerald-50 to-emerald-100 dark:from-emerald-500/10 dark:to-emerald-500/20'
|
|
case PROVIDER_ANTHROPIC:
|
|
return 'bg-gradient-to-br from-orange-50 to-amber-100 dark:from-orange-500/10 dark:to-amber-500/20'
|
|
case PROVIDER_GEMINI:
|
|
return 'bg-gradient-to-br from-sky-50 to-indigo-100 dark:from-sky-500/10 dark:to-indigo-500/20'
|
|
default:
|
|
return 'bg-gradient-to-br from-gray-100 to-gray-200 dark:from-dark-700 dark:to-dark-600'
|
|
}
|
|
}
|