feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强
This commit is contained in:
@@ -1,105 +1,152 @@
|
||||
/**
|
||||
* Admin 后台 - 监控面板
|
||||
* Admin 后台 - 监控面板(报刊风)
|
||||
*
|
||||
* 响应式布局: 移动端单列,桌面端三列
|
||||
* 触摸友好: 卡片可点击查看详情
|
||||
* 功能:
|
||||
* - /health 存活检查(自动:30 秒一轮;可手动刷新)
|
||||
* - /health/ready 数据库就绪检查
|
||||
* - 服务名 / 版本 / 运行时间 / 检查项(后端返回什么就展示什么)
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchHealth } from '../dal'
|
||||
import { SectionHeader } from '../components'
|
||||
import { api } from '../api'
|
||||
import { Card, CardBody, CardHeader, SectionHeader, Alert, Spinner, Badge } from '../components'
|
||||
|
||||
export default function MonitoringPage() {
|
||||
const [health, setHealth] = useState<any>(null)
|
||||
const [ready, setReady] = useState<'ready' | 'not_ready' | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastCheck, setLastCheck] = useState<string>('')
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const [h, r] = await Promise.allSettled([
|
||||
fetchHealth(),
|
||||
api.get<{ status: string }>('/health/ready'),
|
||||
])
|
||||
setHealth(h.status === 'fulfilled' ? h.value : null)
|
||||
setReady(r.status === 'fulfilled' ? (r.value?.status as 'ready' | 'not_ready') : null)
|
||||
if (h.status === 'rejected') {
|
||||
setError(h.reason instanceof Error ? h.reason.message : '无法连接到后端')
|
||||
}
|
||||
setLastCheck(new Date().toLocaleTimeString('zh-CN', { hour12: false }))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchHealth().then(setHealth).finally(() => setLoading(false))
|
||||
}, [])
|
||||
refresh()
|
||||
const t = setInterval(refresh, 30_000)
|
||||
return () => clearInterval(t)
|
||||
}, [refresh])
|
||||
|
||||
const alive = health?.status === 'healthy' || health?.status === 'ok'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SectionHeader
|
||||
title="系统监控"
|
||||
description="查看服务健康状态"
|
||||
description="存活与数据库就绪检查,每 30 秒自动巡检一次。"
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="h-24 animate-pulse rounded-lg bg-gray-800" />
|
||||
))}
|
||||
</div>
|
||||
) : health ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{/* 服务状态 */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
|
||||
<div className="text-xs text-gray-500 mb-2">状态</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block h-3 w-3 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="text-lg font-bold text-emerald-400">{health.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xs text-ink-400">
|
||||
{lastCheck && `最近巡检 ${lastCheck}`}
|
||||
</span>
|
||||
<button onClick={refresh} disabled={loading} className="btn btn-sm">
|
||||
{loading ? (<><Spinner /> 检查中</>) : '立即巡检'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 服务名称 */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
|
||||
<div className="text-xs text-gray-500 mb-2">服务</div>
|
||||
<div className="text-lg font-bold text-white">{health.service || 'profeto'}</div>
|
||||
</div>
|
||||
|
||||
{/* 数据库 */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
|
||||
<div className="text-xs text-gray-500 mb-2">数据库</div>
|
||||
<div className="text-lg font-bold text-blue-400">11 张表</div>
|
||||
</div>
|
||||
|
||||
{/* 版本 */}
|
||||
{health.version && (
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
|
||||
<div className="text-xs text-gray-500 mb-2">版本</div>
|
||||
<div className="text-lg font-bold text-gray-200">{health.version}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 运行时间 */}
|
||||
{health.uptime_seconds && (
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
|
||||
<div className="text-xs text-gray-500 mb-2">运行时间</div>
|
||||
<div className="text-lg font-bold text-gray-200">
|
||||
{Math.floor(health.uptime_seconds / 3600)}h {Math.floor((health.uptime_seconds % 3600) / 60)}m
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 检查项 */}
|
||||
{health.checks && Object.keys(health.checks).length > 0 && (
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-900 p-5">
|
||||
<div className="text-xs text-gray-500 mb-2">健康检查</div>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(health.checks).map(([key, val]) => (
|
||||
<div key={key} className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400 text-xs">{key}</span>
|
||||
<span className={`text-xs font-medium ${val === 'pass' ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{String(val)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-6 text-center">
|
||||
<p className="text-red-400 text-lg font-medium">无法连接到后端</p>
|
||||
<p className="text-red-400/70 text-sm mt-1">请检查服务是否正常运行</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="mt-4 rounded-md bg-red-500/20 px-4 py-2 text-sm text-red-300 hover:bg-red-500/30 min-h-[44px]"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<Alert
|
||||
kind="error"
|
||||
title="无法连接到后端"
|
||||
message={`${error}\n请确认服务是否正常运行,以及管理员密钥是否需要配置。`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{/* 存活状态 */}
|
||||
<div className="border border-ink-900 bg-paper-50 p-5">
|
||||
<div className="text-2xs tracking-[0.2em] text-ink-400">存活状态</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 ${alive ? 'bg-ink-900' : 'bg-press'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={`font-serif text-xl font-bold ${alive ? 'text-ink-900' : 'text-press'}`}>
|
||||
{health ? (alive ? '正常' : String(health.status)) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据库就绪 */}
|
||||
<div className="border border-ink-900 bg-paper-50 p-5">
|
||||
<div className="text-2xs tracking-[0.2em] text-ink-400">数据库就绪</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 ${ready === 'ready' ? 'bg-ink-900' : ready === null ? 'bg-ink-300' : 'bg-press'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className={`font-serif text-xl font-bold ${ready === 'not_ready' ? 'text-press' : 'text-ink-900'}`}
|
||||
>
|
||||
{ready === 'ready' ? '就绪' : ready === 'not_ready' ? '未就绪' : '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服务名 */}
|
||||
<div className="border border-ink-900 bg-paper-50 p-5">
|
||||
<div className="text-2xs tracking-[0.2em] text-ink-400">服务</div>
|
||||
<div className="mt-2 font-serif text-xl font-bold text-ink-900">
|
||||
{health?.service || 'profeto'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本 */}
|
||||
{health?.version && (
|
||||
<div className="border border-ink-900 bg-paper-50 p-5">
|
||||
<div className="text-2xs tracking-[0.2em] text-ink-400">版本</div>
|
||||
<div className="mt-2 font-serif text-xl font-bold tabular-nums text-ink-900">
|
||||
{health.version}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 运行时间 */}
|
||||
{health?.uptime_seconds != null && (
|
||||
<div className="border border-ink-900 bg-paper-50 p-5">
|
||||
<div className="text-2xs tracking-[0.2em] text-ink-400">运行时间</div>
|
||||
<div className="mt-2 font-serif text-xl font-bold tabular-nums text-ink-900">
|
||||
{Math.floor(health.uptime_seconds / 3600)}h{' '}
|
||||
{Math.floor((health.uptime_seconds % 3600) / 60)}m
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 检查项 */}
|
||||
{health?.checks && Object.keys(health.checks).length > 0 && (
|
||||
<div className="border border-ink-900 bg-paper-50 p-5 sm:col-span-2 lg:col-span-1">
|
||||
<div className="text-2xs tracking-[0.2em] text-ink-400">健康检查</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
{Object.entries(health.checks).map(([key, val]) => (
|
||||
<div key={key} className="flex items-center justify-between text-sm">
|
||||
<span className="text-2xs text-ink-500">{key}</span>
|
||||
<Badge status={String(val) === 'pass' ? 'success' : 'error'}>
|
||||
{String(val)}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user