2026-09-01 13:13:40 +08:00
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
2026-09-03 00:44:38 +08:00
|
|
|
|
import { Avatar, Card } from '@heroui/react'
|
|
|
|
|
|
import { hubDashboard, hubDashboardStream } from '../../api/client'
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
|
|
interface BotInfo {
|
|
|
|
|
|
self_id: string
|
|
|
|
|
|
adapter: string
|
|
|
|
|
|
nick: string
|
|
|
|
|
|
avatar: string
|
|
|
|
|
|
ws_connected: boolean
|
|
|
|
|
|
online: boolean | null
|
|
|
|
|
|
msg_recv: number | null
|
|
|
|
|
|
msg_sent: number | null
|
|
|
|
|
|
}
|
|
|
|
|
|
interface SystemInfo {
|
|
|
|
|
|
hostname: string
|
|
|
|
|
|
os: string
|
|
|
|
|
|
python: string
|
|
|
|
|
|
nonebot: string
|
|
|
|
|
|
boot_time: number
|
|
|
|
|
|
system_uptime_seconds: number
|
|
|
|
|
|
bot_uptime_seconds: number
|
|
|
|
|
|
}
|
|
|
|
|
|
interface CpuInfo {
|
|
|
|
|
|
percent: number
|
|
|
|
|
|
per_core: number[]
|
|
|
|
|
|
logical: number | null
|
|
|
|
|
|
physical: number | null
|
|
|
|
|
|
brand: string
|
|
|
|
|
|
load_avg: number[] | null
|
|
|
|
|
|
}
|
|
|
|
|
|
interface MemInfo {
|
|
|
|
|
|
percent: number
|
|
|
|
|
|
used: number
|
|
|
|
|
|
total: number
|
|
|
|
|
|
available: number
|
|
|
|
|
|
process_rss: number
|
|
|
|
|
|
process_percent: number
|
|
|
|
|
|
}
|
|
|
|
|
|
interface ProcessInfo {
|
|
|
|
|
|
pid: number
|
|
|
|
|
|
name: string
|
|
|
|
|
|
rss: number
|
|
|
|
|
|
cpu_percent: number
|
|
|
|
|
|
memory_percent: number
|
|
|
|
|
|
create_time: number
|
|
|
|
|
|
}
|
|
|
|
|
|
interface DiskInfo {
|
|
|
|
|
|
mount: string
|
|
|
|
|
|
device: string
|
|
|
|
|
|
percent: number
|
|
|
|
|
|
used: number
|
|
|
|
|
|
total: number
|
|
|
|
|
|
}
|
2026-09-03 00:44:38 +08:00
|
|
|
|
interface NicInfo {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
up: boolean
|
|
|
|
|
|
sent: number
|
|
|
|
|
|
recv: number
|
|
|
|
|
|
ip: string
|
|
|
|
|
|
}
|
|
|
|
|
|
interface NetworkInfo {
|
|
|
|
|
|
sent: number
|
|
|
|
|
|
recv: number
|
|
|
|
|
|
sent_rate: number
|
|
|
|
|
|
recv_rate: number
|
|
|
|
|
|
packets_sent: number
|
|
|
|
|
|
packets_recv: number
|
|
|
|
|
|
interfaces: NicInfo[]
|
|
|
|
|
|
}
|
|
|
|
|
|
interface BackendInfo {
|
|
|
|
|
|
nonebot_version: string
|
|
|
|
|
|
plugin_count: number
|
|
|
|
|
|
python: string
|
|
|
|
|
|
driver: string
|
|
|
|
|
|
driver_type: string
|
|
|
|
|
|
}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
interface DashboardData {
|
|
|
|
|
|
ok: boolean
|
|
|
|
|
|
bots: BotInfo[]
|
|
|
|
|
|
system: SystemInfo
|
|
|
|
|
|
cpu: CpuInfo
|
|
|
|
|
|
memory: MemInfo
|
|
|
|
|
|
process: ProcessInfo
|
|
|
|
|
|
disk: DiskInfo[]
|
2026-09-03 00:44:38 +08:00
|
|
|
|
network: NetworkInfo
|
|
|
|
|
|
backend: BackendInfo
|
2026-09-01 13:13:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fmtBytes(n: number): string {
|
|
|
|
|
|
if (!Number.isFinite(n)) return '—'
|
|
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
|
|
|
|
let v = n
|
|
|
|
|
|
let i = 0
|
|
|
|
|
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
|
|
|
|
|
return v.toFixed(1) + ' ' + units[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
function fmtRate(n: number): string {
|
|
|
|
|
|
if (!Number.isFinite(n) || n < 0) return '0 B/s'
|
|
|
|
|
|
return fmtBytes(n) + '/s'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 13:13:40 +08:00
|
|
|
|
function fmtUptime(sec: number): string {
|
|
|
|
|
|
if (!Number.isFinite(sec)) return '—'
|
|
|
|
|
|
const d = Math.floor(sec / 86400)
|
|
|
|
|
|
const h = Math.floor((sec % 86400) / 3600)
|
|
|
|
|
|
const m = Math.floor((sec % 3600) / 60)
|
|
|
|
|
|
const parts: string[] = []
|
|
|
|
|
|
if (d) parts.push(d + ' 天')
|
|
|
|
|
|
if (h) parts.push(h + ' 小时')
|
|
|
|
|
|
parts.push(m + ' 分')
|
|
|
|
|
|
return parts.join(' ')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Bar({ value, color }: { value: number; color?: string }) {
|
|
|
|
|
|
const v = Math.max(0, Math.min(100, value))
|
|
|
|
|
|
const c = color || (v >= 80 ? 'bg-rose-500' : v >= 60 ? 'bg-amber-500' : 'bg-indigo-500')
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="h-2 rounded-full bg-slate-100 overflow-hidden">
|
|
|
|
|
|
<div className={'h-full rounded-full transition-all ' + c} style={{ width: v + '%' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function coreColor(v: number): string {
|
|
|
|
|
|
if (v >= 80) return 'bg-rose-500'
|
|
|
|
|
|
if (v >= 60) return 'bg-amber-500'
|
|
|
|
|
|
if (v >= 30) return 'bg-indigo-500'
|
|
|
|
|
|
return 'bg-emerald-500'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex items-center justify-between border-b border-slate-100 pb-1.5">
|
|
|
|
|
|
<span className="text-xs text-slate-400">{label}</span>
|
|
|
|
|
|
<span className="text-sm font-medium text-slate-700 text-right">{value || '—'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function SectionTitle({ title, desc }: { title: string; desc: string }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-1">
|
|
|
|
|
|
<h3 className="text-base font-semibold text-slate-800">{title}</h3>
|
|
|
|
|
|
<p className="text-xs text-slate-400">{desc}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 13:13:40 +08:00
|
|
|
|
export default function HomePage() {
|
|
|
|
|
|
const [data, setData] = useState<DashboardData | null>(null)
|
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
|
const refreshingRef = useRef(false)
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = useCallback(async (silent = false) => {
|
|
|
|
|
|
if (refreshingRef.current) return
|
|
|
|
|
|
refreshingRef.current = true
|
|
|
|
|
|
if (!silent) setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const d = await hubDashboard()
|
|
|
|
|
|
if (!d || d.ok === false) throw new Error((d && (d as any).msg) || '采集失败')
|
|
|
|
|
|
setData(d)
|
|
|
|
|
|
setError('')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
setError(e.message || '加载失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
refreshingRef.current = false
|
|
|
|
|
|
if (!silent) setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-09-03 00:44:38 +08:00
|
|
|
|
refresh(true) // 初始静默加载,快速出首屏
|
|
|
|
|
|
const dispose = hubDashboardStream({
|
|
|
|
|
|
onData: (d) => {
|
|
|
|
|
|
if (d && d.ok !== false) { setData(d); setError('') }
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
return () => { dispose() }
|
2026-09-01 13:13:40 +08:00
|
|
|
|
}, [refresh])
|
|
|
|
|
|
|
|
|
|
|
|
const cpu = data?.cpu
|
|
|
|
|
|
const mem = data?.memory
|
|
|
|
|
|
const proc = data?.process
|
|
|
|
|
|
const sys = data?.system
|
2026-09-03 00:44:38 +08:00
|
|
|
|
const net = data?.network
|
|
|
|
|
|
const backend = data?.backend
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-slate-800">总览</h2>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<p className="text-sm text-slate-400">主机与 BOT 综合状态(每 2 秒刷新)</p>
|
2026-09-01 13:13:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error && !data && <Card className="border border-red-200 bg-red-50"><Card.Content><p className="text-sm text-red-600">{error}</p></Card.Content></Card>}
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
{/* 1. 主机规格信息:系统 / 处理器 / 内存 / 硬盘 */}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
<Card className="border border-slate-200 bg-white">
|
|
|
|
|
|
<Card.Header>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<SectionTitle title="主机规格信息" desc="系统 / 处理器 / 内存 / 硬盘" />
|
|
|
|
|
|
</Card.Header>
|
|
|
|
|
|
<Card.Content>
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="mb-2 text-sm font-medium text-slate-700">系统</div>
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<InfoRow label="主机名" value={sys?.hostname || ''} />
|
|
|
|
|
|
<InfoRow label="操作系统" value={sys?.os || ''} />
|
|
|
|
|
|
<InfoRow label="运行时长" value={sys ? fmtUptime(sys.system_uptime_seconds) : '—'} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="mb-2 text-sm font-medium text-slate-700">处理器</div>
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<InfoRow label="型号" value={cpu?.brand || ''} />
|
|
|
|
|
|
<InfoRow label="逻辑核心" value={cpu?.logical != null ? cpu.logical + ' 核' : '—'} />
|
|
|
|
|
|
<InfoRow label="物理核心" value={cpu?.physical != null ? cpu.physical + ' 核' : '—'} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="mb-2 text-sm font-medium text-slate-700">内存</div>
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<InfoRow label="总量" value={mem ? fmtBytes(mem.total) : '—'} />
|
|
|
|
|
|
<InfoRow label="可用" value={mem ? fmtBytes(mem.available) : '—'} />
|
|
|
|
|
|
<InfoRow label="本进程占用" value={mem ? fmtBytes(mem.process_rss) : '—'} />
|
|
|
|
|
|
</div>
|
2026-09-01 13:13:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<div className="mb-2 text-sm font-medium text-slate-700">硬盘</div>
|
|
|
|
|
|
{!data ? (
|
|
|
|
|
|
<p className="text-sm text-slate-400">加载中…</p>
|
|
|
|
|
|
) : !data.disk.length ? (
|
|
|
|
|
|
<p className="text-sm text-slate-400">未获取到磁盘信息</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
|
{data.disk.map((d) => (
|
|
|
|
|
|
<div key={d.mount} className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<span className="truncate text-xs font-medium text-slate-600" title={d.mount}>{d.mount}</span>
|
|
|
|
|
|
<span className="text-xs text-slate-500">{d.percent}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-2"><Bar value={d.percent} /></div>
|
|
|
|
|
|
<p className="mt-2 text-xs text-slate-400">{fmtBytes(d.used)} / {fmtBytes(d.total)}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card.Content>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 2. 资源占用信息:CPU / 内存 / 网络 */}
|
|
|
|
|
|
<Card className="border border-slate-200 bg-white">
|
|
|
|
|
|
<Card.Header>
|
|
|
|
|
|
<SectionTitle title="资源占用信息" desc="CPU / 内存 / 网络实时占用" />
|
2026-09-01 13:13:40 +08:00
|
|
|
|
</Card.Header>
|
|
|
|
|
|
<Card.Content>
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
|
|
|
|
{/* CPU */}
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<span className="text-sm font-medium text-slate-700">CPU 使用率{cpu?.logical ? ' ' + cpu.logical + ' 核' : ''}</span>
|
|
|
|
|
|
<span className="text-sm font-semibold text-slate-800">{cpu ? cpu.percent + '%' : '—'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-3"><Bar value={cpu?.percent || 0} /></div>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<p className="mt-2 text-xs text-slate-400">负载: {cpu?.load_avg ? cpu.load_avg.join(' / ') : '—'}</p>
|
2026-09-01 13:13:40 +08:00
|
|
|
|
{cpu?.per_core?.length ? (
|
|
|
|
|
|
<div className="mt-3 grid grid-cols-4 gap-2">
|
|
|
|
|
|
{cpu.per_core.map((v, i) => (
|
|
|
|
|
|
<div key={i} className="flex flex-col items-center rounded-lg border border-slate-100 bg-slate-50 px-2 py-1.5">
|
|
|
|
|
|
<span className="text-xs text-slate-400">{i}</span>
|
|
|
|
|
|
<div className={'mt-1 h-1.5 w-full rounded-full ' + coreColor(v)} />
|
|
|
|
|
|
<span className="mt-1 text-[10px] text-slate-500">{v.toFixed(0)}%</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 内存 */}
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<span className="text-sm font-medium text-slate-700">内存使用</span>
|
|
|
|
|
|
<span className="text-sm font-semibold text-slate-800">{mem ? mem.percent + '%' : '—'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-3"><Bar value={mem?.percent || 0} /></div>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<p className="mt-2 text-xs text-slate-400">已用 {mem ? fmtBytes(mem.used) : '—'} · 可用 {mem ? fmtBytes(mem.available) : '—'}</p>
|
2026-09-01 13:13:40 +08:00
|
|
|
|
<p className="mt-1 text-xs text-slate-400">共 {mem ? fmtBytes(mem.total) : '—'}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
{/* 网络 */}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<span className="text-sm font-medium text-slate-700">网络流量</span>
|
|
|
|
|
|
{net?.interfaces?.length ? <span className="text-xs text-slate-400">{net.interfaces.length} 网卡</span> : null}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
</div>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<div className="mt-3 space-y-2 text-xs">
|
|
|
|
|
|
<div className="flex justify-between"><dt className="text-slate-400">↓ 下行速率</dt><dd className="text-slate-700">{net ? fmtRate(net.recv_rate) : '—'}</dd></div>
|
|
|
|
|
|
<div className="flex justify-between"><dt className="text-slate-400">↑ 上行速率</dt><dd className="text-slate-700">{net ? fmtRate(net.sent_rate) : '—'}</dd></div>
|
|
|
|
|
|
<div className="flex justify-between"><dt className="text-slate-400">累计下行</dt><dd className="text-slate-700">{net ? fmtBytes(net.recv) : '—'}</dd></div>
|
|
|
|
|
|
<div className="flex justify-between"><dt className="text-slate-400">累计上行</dt><dd className="text-slate-700">{net ? fmtBytes(net.sent) : '—'}</dd></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 本进程 */}
|
|
|
|
|
|
<div className="mt-4 rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="mb-2 text-sm font-medium text-slate-700">运行进程(bot)</div>
|
|
|
|
|
|
<dl className="grid grid-cols-2 gap-2 text-xs sm:grid-cols-3 lg:grid-cols-5">
|
|
|
|
|
|
<div className="flex flex-col"><dt className="text-slate-400">PID</dt><dd className="text-slate-700">{proc?.pid ?? '—'}</dd></div>
|
|
|
|
|
|
<div className="flex flex-col"><dt className="text-slate-400">进程名</dt><dd className="text-slate-700">{proc?.name || '—'}</dd></div>
|
|
|
|
|
|
<div className="flex flex-col"><dt className="text-slate-400">CPU</dt><dd className="text-slate-700">{proc?.cpu_percent != null ? proc.cpu_percent + '%' : '—'}</dd></div>
|
|
|
|
|
|
<div className="flex flex-col"><dt className="text-slate-400">内存占用</dt><dd className="text-slate-700">{proc?.memory_percent != null ? proc.memory_percent + '%' : '—'}</dd></div>
|
|
|
|
|
|
<div className="flex flex-col"><dt className="text-slate-400">RSS</dt><dd className="text-slate-700">{proc ? fmtBytes(proc.rss) : '—'}</dd></div>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card.Content>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 3. BOT 后端信息:NoneBot 版本 / 驱动方式 / 装载插件数 */}
|
|
|
|
|
|
<Card className="border border-slate-200 bg-white">
|
|
|
|
|
|
<Card.Header>
|
|
|
|
|
|
<SectionTitle title="BOT 后端信息" desc="NoneBot 版本 / 驱动方式 / 装载插件数" />
|
|
|
|
|
|
</Card.Header>
|
|
|
|
|
|
<Card.Content>
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="text-xs text-slate-400">NoneBot 版本</div>
|
|
|
|
|
|
<div className="mt-1 text-lg font-semibold text-slate-800">{backend?.nonebot_version || '—'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="text-xs text-slate-400">驱动方式</div>
|
|
|
|
|
|
<div className="mt-1 truncate text-sm font-medium text-slate-700" title={backend?.driver}>{backend?.driver || '—'}</div>
|
|
|
|
|
|
<div className="text-xs text-slate-400">{backend?.driver_type || ''}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="text-xs text-slate-400">装载插件数</div>
|
|
|
|
|
|
<div className="mt-1 text-lg font-semibold text-slate-800">{backend?.plugin_count ?? '—'} 个</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="rounded-xl border border-slate-200 p-4">
|
|
|
|
|
|
<div className="text-xs text-slate-400">Python</div>
|
|
|
|
|
|
<div className="mt-1 truncate text-sm font-medium text-slate-700" title={backend?.python}>{backend?.python || '—'}</div>
|
|
|
|
|
|
<div className="text-xs text-slate-400">bot 运行 {sys ? fmtUptime(sys.bot_uptime_seconds) : '—'}</div>
|
2026-09-01 13:13:40 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card.Content>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
{/* 4. BOT 协议端信息:登录账号昵称 / 账号 / 适配器类型 */}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
<Card className="border border-slate-200 bg-white">
|
|
|
|
|
|
<Card.Header>
|
2026-09-03 00:44:38 +08:00
|
|
|
|
<SectionTitle title="BOT 协议端信息" desc="当前登录的账户昵称 / 账号 / 适配器类型" />
|
2026-09-01 13:13:40 +08:00
|
|
|
|
</Card.Header>
|
|
|
|
|
|
<Card.Content>
|
|
|
|
|
|
{!data ? (
|
|
|
|
|
|
<p className="text-sm text-slate-400">加载中…</p>
|
|
|
|
|
|
) : !data.bots.length ? (
|
|
|
|
|
|
<p className="text-sm text-slate-400">暂无在线 QQ 账号</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{data.bots.map(bot => (
|
|
|
|
|
|
<div key={bot.self_id} className="flex items-center gap-3 rounded-xl border border-slate-100 px-4 py-3">
|
|
|
|
|
|
<Avatar size="sm">
|
|
|
|
|
|
<Avatar.Image src={bot.avatar} alt={bot.nick} />
|
|
|
|
|
|
<Avatar.Fallback>{(bot.nick || bot.self_id).charAt(0).toUpperCase()}</Avatar.Fallback>
|
|
|
|
|
|
</Avatar>
|
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
|
<div className="truncate text-sm font-medium text-slate-800">{bot.nick}</div>
|
|
|
|
|
|
<div className="truncate text-xs text-slate-400">{bot.self_id} · {bot.adapter}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
{bot.msg_recv != null && <span className="text-xs text-slate-400">收 {bot.msg_recv}</span>}
|
|
|
|
|
|
{bot.msg_sent != null && <span className="text-xs text-slate-400">发 {bot.msg_sent}</span>}
|
2026-09-03 00:44:38 +08:00
|
|
|
|
{bot.online != null && <span className="text-xs text-slate-400">{bot.online ? '在线' : '离线'}</span>}
|
2026-09-01 13:13:40 +08:00
|
|
|
|
<span className="inline-flex items-center gap-1.5 text-xs">
|
|
|
|
|
|
<span className={'size-2 rounded-full ' + (bot.ws_connected ? 'bg-emerald-500' : 'bg-slate-300')} />
|
|
|
|
|
|
<span className={bot.ws_connected ? 'text-emerald-600' : 'text-slate-400'}>{bot.ws_connected ? '已连接' : '未连接'}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card.Content>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|