独立监控核心和模块

This commit is contained in:
zhongluofeng
2026-07-23 19:27:40 +08:00
parent 62db8e1982
commit e8267c244f
14 changed files with 2516 additions and 17 deletions
+586 -17
View File
@@ -1,24 +1,593 @@
<script setup lang="ts">
import { Activity } from '@lucide/vue'
import {
Activity, Play, Square, RefreshCw, Loader2, Cpu, MemoryStick,
Gauge, HardDrive, Settings as SettingsIcon, AlertTriangle,
ShieldCheck, ShieldAlert, Zap, Thermometer, Clock, ChevronDown,
} from '@lucide/vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { toast } from 'vue-sonner'
import { useMonitorStore, type SensorEntry, type SensorGroup, type ConnectionState } from '@/stores/monitorStore'
import { useModuleTabs } from '@/lib/useModuleTabs'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
import { Progress } from '@/components/ui/progress'
import { Separator } from '@/components/ui/separator'
const store = useMonitorStore()
// ===== Tab 配置(注册到 TitleBar 浮动切换器) =====
const activeTab = ref('overview')
const tabsListRef = useModuleTabs(activeTab, [
{ value: 'overview', label: '概览' },
{ value: 'details', label: '详细' },
{ value: 'settings', label: '设置' },
])
// ===== 关键指标历史 buffer(用于概览页 sparkline,不引入外部图表库) =====
// 容量 30 点,约对应 30 秒(fast 通道 1s 推送一次)
const HISTORY_LEN = 30
const cpuTempHistory = ref<number[]>([])
const cpuLoadHistory = ref<number[]>([])
const memLoadHistory = ref<number[]>([])
const gpuLoadHistory = ref<number[]>([])
function pushHistory(buf: typeof cpuTempHistory, v: number | null) {
if (v == null || !isFinite(v)) return
buf.value.push(v)
if (buf.value.length > HISTORY_LEN) buf.value.shift()
}
// 监听快照变化,提取关键指标入历史
watch(() => store.snapshot, (snap) => {
if (!snap) return
pushHistory(cpuTempHistory, store.findSensorValue('cpu', { name: 'CPU Package', type: 'temperature' }))
pushHistory(cpuLoadHistory, store.findSensorValue('cpu', { name: 'CPU Total', type: 'load' }))
// Memory 分组有两条同名 "Memory" 负载传感器(Virtual Memory + Total Memory),优先取 Total Memory
pushHistory(memLoadHistory, store.findSensorValue('memory', { name: 'Memory', hardwareName: 'Total Memory', type: 'load' }))
// Intel GPU 3D 负载传感器名为 "D3D 3D";部分 GPU 为 "GPU Core"
pushHistory(gpuLoadHistory,
store.findSensorValue('gpuintel', { name: 'D3D 3D', type: 'load' }) ??
store.findSensorValue('gpuintel', { name: 'GPU Core', type: 'load' }) ??
store.findSensorValue('gpuintel', { type: 'load' }),
)
}, { deep: false })
// ===== 关键指标 computed =====
const cpuTemp = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'temperature' }))
const cpuLoad = computed(() => store.findSensorValue('cpu', { name: 'CPU Total', type: 'load' }))
const memLoad = computed(() => store.findSensorValue('memory', { name: 'Memory', hardwareName: 'Total Memory', type: 'load' }))
const gpuLoad = computed(() =>
store.findSensorValue('gpuintel', { name: 'D3D 3D', type: 'load' }) ??
store.findSensorValue('gpuintel', { name: 'GPU Core', type: 'load' }) ??
store.findSensorValue('gpuintel', { type: 'load' }),
)
const storageTemp = computed(() => store.findSensorValue('storage', { type: 'temperature' }))
// ===== 工具函数 =====
/** 格式化数值:整数型指标(负载/温度)保留 0 位,浮点型(电压/功率)保留 2 位 */
function fmt(v: number | null, digits = 1): string {
if (v == null || !isFinite(v)) return '--'
return v.toFixed(digits)
}
/** 温度颜色:绿(<50) → 黄(<70) → 橙(<85) → 红(>=85) */
function tempColor(t: number | null): string {
if (t == null) return 'text-muted-foreground'
if (t < 50) return 'text-emerald-500'
if (t < 70) return 'text-yellow-500'
if (t < 85) return 'text-orange-500'
return 'text-red-500'
}
/** 负载颜色:蓝(<50) → 紫(<80) → 红(>=80) */
function loadColor(v: number | null): string {
if (v == null) return 'text-muted-foreground'
if (v < 50) return 'text-sky-500'
if (v < 80) return 'text-violet-500'
return 'text-red-500'
}
/** 传感器类型 → 中文标签 */
const typeLabels: Record<string, string> = {
temperature: '温度',
load: '负载',
power: '功率',
voltage: '电压',
fan: '风扇',
clock: '时钟',
data: '容量',
smalldata: '容量',
throughput: '吞吐',
level: '等级',
control: '控制',
frequency: '频率',
factor: '因子',
timespan: '时长',
energy: '能量',
noise: '噪声',
conductivity: '电导率',
humidity: '湿度',
flow: '流量',
}
function typeLabel(t: string): string {
return typeLabels[t] ?? t
}
/** 分组 id → 显示名 + 图标组件 */
const groupMeta: Record<string, { name: string; icon: typeof Cpu }> = {
cpu: { name: 'CPU', icon: Cpu },
memory: { name: '内存', icon: MemoryStick },
gpuintel: { name: 'GPU', icon: Gauge },
gpuamd: { name: 'GPU', icon: Gauge },
gpunvidia: { name: 'GPU', icon: Gauge },
storage: { name: '存储', icon: HardDrive },
motherboard: { name: '主板', icon: Activity },
superio: { name: '超级 IO', icon: Activity },
embeddedcontroller: { name: '嵌入式控制器', icon: Activity },
battery: { name: '电池', icon: Activity },
network: { name: '网络', icon: Activity },
psu: { name: '电源', icon: Zap },
}
function groupDisplayName(id: string, fallback: string): string {
return groupMeta[id]?.name ?? fallback
}
function groupIcon(id: string): typeof Cpu {
return groupMeta[id]?.icon ?? Activity
}
// ===== 连接状态徽章 =====
const stateMeta: Record<ConnectionState, { text: string; class: string }> = {
idle: { text: '未启动', class: 'bg-muted text-muted-foreground' },
loading: { text: '启动中', class: 'bg-yellow-500/15 text-yellow-600 dark:text-yellow-400' },
connected: { text: '已连接', class: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400' },
disconnected: { text: '已断开', class: 'bg-orange-500/15 text-orange-600 dark:text-orange-400' },
error: { text: '错误', class: 'bg-red-500/15 text-red-600 dark:text-red-400' },
}
// ===== sparkline 路径生成(无外部依赖) =====
function sparklinePath(data: number[], opts: { min?: number; max?: number; w?: number; h?: number } = {}): string {
if (data.length < 2) return ''
const w = opts.w ?? 100
const h = opts.h ?? 28
const min = opts.min ?? Math.min(...data)
const max = opts.max ?? Math.max(...data)
const range = max - min || 1
const step = w / (data.length - 1)
return data.map((v, i) => {
const x = i * step
const y = h - ((v - min) / range) * h
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`
}).join(' ')
}
// ===== 分组列表(详细页用) =====
const groups = computed<SensorGroup[]>(() => store.snapshot?.groups ?? [])
/** 按 hardwareName 子分组,再按 type 二级分组(详细页用) */
function groupSensors(sensors: SensorEntry[]): { hardware: string; byType: { type: string; items: SensorEntry[] }[] }[] {
const byHw = new Map<string, SensorEntry[]>()
for (const s of sensors) {
if (!byHw.has(s.hardwareName)) byHw.set(s.hardwareName, [])
byHw.get(s.hardwareName)!.push(s)
}
return Array.from(byHw.entries()).map(([hw, items]) => {
const byType = new Map<string, SensorEntry[]>()
for (const s of items) {
if (!byType.has(s.type)) byType.set(s.type, [])
byType.get(s.type)!.push(s)
}
return {
hardware: hw,
byType: Array.from(byType.entries()).map(([type, list]) => ({ type, items: list })),
}
})
}
// ===== Accordion 折叠状态 =====
const accordionValue = ref<string[]>([])
// ===== 控制操作 =====
async function handleStart() {
await store.start()
if (store.connState === 'connected') toast.success('Kernel 已启动')
else if (store.errorMsg) toast.error('启动失败', { description: store.errorMsg })
}
async function handleStop() {
await store.stop()
toast.success('Kernel 已停止')
}
async function handleRefresh() {
await store.refreshStatus()
if (store.status?.ready) {
await store.fetchSnapshot()
toast.success('已刷新')
}
}
// 生命周期
onMounted(() => {
store.init()
})
onUnmounted(() => {
store.dispose()
})
// 当 Kernel 从未就绪变成就绪时,主动拉一次快照填充 UI
watch(() => store.status?.ready, (ready, prev) => {
if (ready && !prev) {
store.fetchSnapshot()
}
})
</script>
<template>
<div class="h-full p-6 overflow-y-auto">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Activity class="h-5 w-5 text-primary" />
硬件监控模块
</CardTitle>
</CardHeader>
<CardContent>
<div class="flex flex-col items-center justify-center h-64 text-muted-foreground">
<Activity class="h-16 w-16 mb-4 opacity-50" />
<p>硬件监控功能开发中...</p>
<p class="text-sm mt-2">支持 CPUGPU内存硬盘等硬件数据监控</p>
<div class="h-full p-6">
<Tabs v-model="activeTab" class="h-full flex flex-col">
<div ref="tabsListRef">
<TabsList class="grid w-full max-w-md grid-cols-3 !bg-transparent !p-0 !shadow-none">
<TabsTrigger value="overview" class="gap-1.5"><Activity class="size-3.5" />概览</TabsTrigger>
<TabsTrigger value="details" class="gap-1.5"><Gauge class="size-3.5" />详细</TabsTrigger>
<TabsTrigger value="settings" class="gap-1.5"><SettingsIcon class="size-3.5" />设置</TabsTrigger>
</TabsList>
</div>
<!-- ===== 概览 ===== -->
<TabsContent value="overview" class="flex-1 mt-4 tab-animate">
<ScrollArea class="h-full pr-3">
<!-- 未启动占位 -->
<div v-if="!store.initialized" key="initializing" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10">
<Loader2 class="size-8 animate-spin" />
<p class="text-sm">正在加载...</p>
</div>
<div v-else-if="store.connState === 'idle'" key="idle" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-3 pt-10">
<Activity class="size-12 opacity-30" />
<p class="text-sm">Kernel 未启动</p>
<Button size="sm" :disabled="store.starting" @click="handleStart">
<Loader2 v-if="store.starting" class="size-3.5 animate-spin" />
<Play v-else class="size-3.5" />启动 Kernel
</Button>
</div>
<div v-else key="content" class="columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid">
<!-- 状态卡片 -->
<Card>
<CardHeader class="pb-3">
<CardTitle class="flex items-center justify-between text-base">
<span class="flex items-center gap-2"><Activity class="size-4 text-primary" />Kernel 状态</span>
<span :class="['text-xs px-2 py-0.5 rounded-full', stateMeta[store.connState].class]">
{{ stateMeta[store.connState].text }}
</span>
</CardTitle>
</CardHeader>
<CardContent class="space-y-2 text-sm">
<div class="grid grid-cols-2 gap-x-4 gap-y-1.5">
<div class="flex justify-between">
<span class="text-muted-foreground">进程</span>
<span>{{ store.status?.running ? '运行中' : '已停止' }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">PID</span>
<span class="font-mono">{{ store.status?.pid ?? '--' }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">就绪</span>
<span>{{ store.status?.ready ? '是' : '否' }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">传感器数</span>
<span>{{ store.status?.sensorCount ?? '--' }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">重启次数</span>
<span>{{ store.status?.restartCount ?? 0 }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">事件数</span>
<span>{{ store.eventCount }}</span>
</div>
</div>
<Separator />
<div class="flex items-center justify-between">
<span class="flex items-center gap-1.5 text-muted-foreground">
<component :is="store.snapshot?.isAdmin ? ShieldCheck : ShieldAlert" class="size-3.5" />
权限
</span>
<Badge :variant="store.snapshot?.isAdmin ? 'default' : 'outline'" :class="store.snapshot?.isAdmin ? 'bg-emerald-500 hover:bg-emerald-500' : ''">
{{ store.snapshot?.isAdmin ? '管理员' : '普通' }}
</Badge>
</div>
<div v-if="!store.snapshot?.isAdmin" class="text-xs text-orange-600 dark:text-orange-400 flex items-start gap-1.5 pt-1">
<AlertTriangle class="size-3.5 mt-0.5 shrink-0" />
<span>非管理员运行部分传感器电压/主板/SMBus可能不可读可在设置页查看降级说明</span>
</div>
<div class="flex items-center gap-2 pt-1">
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping" @click="handleRefresh">
<RefreshCw class="size-3" />刷新
</Button>
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping" @click="handleStop">
<Square class="size-3" />停止
</Button>
</div>
</CardContent>
</Card>
<!-- CPU 关键指标 -->
<Card>
<CardHeader class="pb-3">
<CardTitle class="flex items-center gap-2 text-base">
<Cpu class="size-4 text-primary" />CPU
</CardTitle>
</CardHeader>
<CardContent class="space-y-3">
<div class="flex items-end justify-between">
<div>
<div class="text-xs text-muted-foreground flex items-center gap-1"><Thermometer class="size-3" />封装温度</div>
<div :class="['text-2xl font-bold tabular-nums', tempColor(cpuTemp)]">
{{ fmt(cpuTemp, 0) }}<span class="text-sm font-normal">°C</span>
</div>
</div>
<svg v-if="cpuTempHistory.length >= 2" :width="100" :height="28" class="opacity-80">
<path :d="sparklinePath(cpuTempHistory)" :stroke="'currentColor'" :class="tempColor(cpuTemp)" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<div>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-muted-foreground flex items-center gap-1"><Gauge class="size-3" />总负载</span>
<span :class="['font-medium tabular-nums', loadColor(cpuLoad)]">{{ fmt(cpuLoad, 0) }}%</span>
</div>
<Progress :model-value="cpuLoad ?? 0" class="h-1.5" />
</div>
</CardContent>
</Card>
<!-- 内存 -->
<Card>
<CardHeader class="pb-3">
<CardTitle class="flex items-center gap-2 text-base">
<MemoryStick class="size-4 text-primary" />内存
</CardTitle>
</CardHeader>
<CardContent class="space-y-3">
<div>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-muted-foreground">内存负载</span>
<span :class="['font-medium tabular-nums', loadColor(memLoad)]">{{ fmt(memLoad, 0) }}%</span>
</div>
<Progress :model-value="memLoad ?? 0" class="h-1.5" />
</div>
<svg v-if="memLoadHistory.length >= 2" :width="'100%'" :height="32" class="opacity-80" preserveAspectRatio="none" viewBox="0 0 100 32">
<path :d="sparklinePath(memLoadHistory, { min: 0, max: 100, w: 100, h: 32 })" stroke="currentColor" class="text-violet-500" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</CardContent>
</Card>
<!-- GPU -->
<Card>
<CardHeader class="pb-3">
<CardTitle class="flex items-center gap-2 text-base">
<Gauge class="size-4 text-primary" />GPU
</CardTitle>
</CardHeader>
<CardContent class="space-y-3">
<div v-if="gpuLoad != null">
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-muted-foreground">3D 负载</span>
<span :class="['font-medium tabular-nums', loadColor(gpuLoad)]">{{ fmt(gpuLoad, 0) }}%</span>
</div>
<Progress :model-value="gpuLoad ?? 0" class="h-1.5" />
</div>
<div v-else class="text-xs text-muted-foreground py-2 text-center">暂无 GPU 数据</div>
</CardContent>
</Card>
<!-- 存储 -->
<Card v-if="storageTemp != null">
<CardHeader class="pb-3">
<CardTitle class="flex items-center gap-2 text-base">
<HardDrive class="size-4 text-primary" />存储
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-xs text-muted-foreground flex items-center gap-1"><Thermometer class="size-3" />温度</div>
<div :class="['text-2xl font-bold tabular-nums', tempColor(storageTemp)]">
{{ fmt(storageTemp, 0) }}<span class="text-sm font-normal">°C</span>
</div>
</CardContent>
</Card>
<!-- 断线提示 -->
<Card v-if="store.connState === 'disconnected'" class="border-orange-500/40">
<CardContent class="pt-4 flex items-start gap-2 text-sm">
<AlertTriangle class="size-4 text-orange-500 mt-0.5 shrink-0" />
<div>
<div class="font-medium text-orange-600 dark:text-orange-400">SSE 连接断开</div>
<div class="text-xs text-muted-foreground mt-0.5">Kernel 可能正在重启等待自动重连...</div>
</div>
</CardContent>
</Card>
<!-- 错误提示 -->
<Card v-if="store.errorMsg" class="border-red-500/40">
<CardContent class="pt-4 flex items-start gap-2 text-sm">
<AlertTriangle class="size-4 text-red-500 mt-0.5 shrink-0" />
<div>
<div class="font-medium text-red-600 dark:text-red-400">错误</div>
<div class="text-xs font-mono mt-0.5 break-all">{{ store.errorMsg }}</div>
</div>
</CardContent>
</Card>
</div>
</ScrollArea>
</TabsContent>
<!-- ===== 详细 ===== -->
<TabsContent value="details" class="flex-1 mt-4 tab-animate">
<div v-if="!store.snapshot" key="no-snapshot" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10">
<Gauge class="size-12 opacity-30" />
<p class="text-sm">无快照数据</p>
<p v-if="store.connState === 'idle'" class="text-xs">请先在概览页启动 Kernel</p>
<Button v-else size="xs" variant="outline" :disabled="!store.status?.ready" @click="store.fetchSnapshot()">
<RefreshCw class="size-3" />拉取快照
</Button>
</div>
</CardContent>
</Card>
<ScrollArea v-else key="details-list" class="h-full pr-3">
<div class="flex items-center justify-between mb-3">
<p class="text-sm text-muted-foreground">{{ groups.length }} 个分组 / {{ store.status?.sensorCount ?? 0 }} 个传感器</p>
<Button size="xs" variant="outline" @click="store.fetchSnapshot()">
<RefreshCw class="size-3" />刷新
</Button>
</div>
<Accordion v-model="accordionValue" type="multiple" class="w-full space-y-2 pb-4">
<Card v-for="g in groups" :key="g.id" class="overflow-hidden py-0">
<AccordionItem :value="g.id" class="border-b-0">
<AccordionTrigger class="px-4 py-2.5 hover:no-underline">
<div class="flex items-center justify-between w-full pr-2">
<span class="flex items-center gap-2">
<component :is="groupIcon(g.id)" class="size-4 text-primary" />
<span class="font-medium">{{ groupDisplayName(g.id, g.name) }}</span>
<Badge variant="outline" class="font-normal">{{ g.sensors.length }}</Badge>
</span>
<span class="text-xs text-muted-foreground">{{ g.id }}</span>
</div>
</AccordionTrigger>
<AccordionContent class="px-4 pb-3 pt-0">
<div v-for="hwGroup in groupSensors(g.sensors)" :key="hwGroup.hardware" class="py-2 border-t first:border-t-0">
<div class="text-xs font-medium text-muted-foreground mb-1.5 flex items-center gap-1">
<ChevronDown class="size-3" />{{ hwGroup.hardware }}
</div>
<div v-for="typeGroup in hwGroup.byType" :key="typeGroup.type" class="mb-2">
<div class="text-[11px] uppercase tracking-wide text-muted-foreground/70 mb-1">{{ typeLabel(typeGroup.type) }}</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-0.5 text-xs">
<div v-for="s in typeGroup.items" :key="s.id" class="flex justify-between items-center py-0.5">
<span class="text-muted-foreground truncate pr-2" :title="s.name">{{ s.name }}</span>
<span class="font-mono tabular-nums shrink-0" :class="{ 'text-muted-foreground/50': s.value == null }">
{{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }}
<span class="text-muted-foreground ml-0.5">{{ s.unit }}</span>
</span>
</div>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Card>
</Accordion>
</ScrollArea>
</TabsContent>
<!-- ===== 设置 ===== -->
<TabsContent value="settings" class="flex-1 mt-4 tab-animate">
<ScrollArea class="h-full pr-3">
<div class="max-w-2xl space-y-4 pb-4">
<!-- 控制 -->
<Card>
<CardHeader>
<CardTitle class="text-base flex items-center gap-2">
<Activity class="size-4 text-primary" />Kernel 控制
</CardTitle>
</CardHeader>
<CardContent class="space-y-3 text-sm">
<div class="flex items-center gap-2 flex-wrap">
<Button size="sm" :disabled="store.starting || store.stopping || store.connState !== 'idle'" @click="handleStart">
<Loader2 v-if="store.starting" class="size-3.5 animate-spin" />
<Play v-else class="size-3.5" />启动
</Button>
<Button size="sm" variant="destructive" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleStop">
<Loader2 v-if="store.stopping" class="size-3.5 animate-spin" />
<Square v-else class="size-3.5" />停止
</Button>
<Button size="sm" variant="outline" :disabled="!store.status?.ready" @click="handleRefresh">
<RefreshCw class="size-3.5" />刷新状态
</Button>
</div>
<div class="text-xs text-muted-foreground">
Kernel ProcessManager 统一管理应用启动时自动拉起崩溃自动重启
</div>
</CardContent>
</Card>
<!-- Kernel 信息 -->
<Card v-if="store.kernelInfo">
<CardHeader>
<CardTitle class="text-base flex items-center gap-2">
<SettingsIcon class="size-4 text-primary" />Kernel 信息
</CardTitle>
</CardHeader>
<CardContent class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">端口</span>
<span class="font-mono">{{ store.kernelInfo.port }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">已安装</span>
<Badge :variant="store.kernelInfo.exists ? 'default' : 'destructive'" :class="store.kernelInfo.exists ? 'bg-emerald-500 hover:bg-emerald-500' : ''">
{{ store.kernelInfo.exists ? '是' : '否' }}
</Badge>
</div>
<div class="flex justify-between items-start gap-2">
<span class="text-muted-foreground shrink-0">路径</span>
<span class="font-mono text-xs text-right break-all" :title="store.kernelInfo.path">{{ store.kernelInfo.path }}</span>
</div>
</CardContent>
</Card>
<!-- 权限与降级说明 -->
<Card>
<CardHeader>
<CardTitle class="text-base flex items-center gap-2">
<ShieldCheck class="size-4 text-primary" />权限与降级
</CardTitle>
</CardHeader>
<CardContent class="space-y-2 text-sm text-muted-foreground">
<p>当前权限<Badge :variant="store.snapshot?.isAdmin ? 'default' : 'outline'" :class="store.snapshot?.isAdmin ? 'bg-emerald-500 hover:bg-emerald-500 ml-1' : 'ml-1'">{{ store.snapshot?.isAdmin ? '管理员' : '普通' }}</Badge></p>
<Separator />
<p class="text-xs leading-relaxed">
LibreHardwareMonitor 访问 SMBus部分 EC 传感器某些 GPU 传感器需要管理员权限
默认非提权运行覆盖大部分 CPU/GPU 温度通过 OHM RPC/WMI 仍可读牺牲部分主板/电压传感器
</p>
<p class="text-xs leading-relaxed text-orange-600 dark:text-orange-400 flex items-start gap-1.5">
<AlertTriangle class="size-3.5 mt-0.5 shrink-0" />
<span>提权策略任务计划程序免 UAC 启动将在阶段五实现当前以普通权限运行</span>
</p>
</CardContent>
</Card>
<!-- 关于 -->
<Card>
<CardHeader>
<CardTitle class="text-base flex items-center gap-2">
<Clock class="size-4 text-primary" />关于
</CardTitle>
</CardHeader>
<CardContent class="text-sm space-y-2 text-muted-foreground">
<p class="text-xs leading-relaxed">
数据源LibreHardwareMonitorLib 0.9.5Native AOT 编译独立进程
</p>
<p class="text-xs leading-relaxed">
通信HTTP + SSE本地 loopbackschemaVersion=1 数据契约
</p>
<p v-if="store.snapshot?.coldStartMs" class="text-xs leading-relaxed">
冷启动耗时{{ (store.snapshot.coldStartMs / 1000).toFixed(1) }}
</p>
</CardContent>
</Card>
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</div>
</template>
</template>
+28
View File
@@ -1,5 +1,6 @@
import type { ModuleConfig } from '@/types/module'
import type { SearchIndexItem } from '@/stores/searchIndex'
import { invoke } from '@tauri-apps/api/core'
const searchItems: SearchIndexItem[] = [
{
@@ -18,5 +19,32 @@ export const moduleConfig: ModuleConfig = {
defaultEnabled: true,
loader: () => import('./MonitorModule.vue'),
searchItems,
// 进程由 MonitorKernel 通过 ProcessManager 统一管理(id='monitor'),
// executable/args 在运行时由后端 monitor_start 确定,此处仅声明 hasProcess 以便禁用时自动停止。
process: {
name: 'ThingHK',
executable: '',
autoStart: false,
restartOnCrash: true,
maxRestarts: 3
},
lifecycle: {
onEnable: async () => {
// 启用模块时拉起 Kernel 并开始 SSE 订阅
try {
await invoke('monitor_start')
} catch {
/* 忽略:可能 Kernel 未安装 */
}
},
onDisable: async () => {
// 禁用模块时停止 SSE 订阅并终止 Kernel 进程
try {
await invoke('monitor_stop')
} catch {
/* 忽略:可能 Kernel 未运行 */
}
}
},
order: 40
}
+256
View File
@@ -0,0 +1,256 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { createLogger } from '@/lib/logger'
const logger = createLogger('monitor')
// ===== 与 Rust 端 / C# Contracts.cs 对应的数据结构(camelCase =====
// schemaVersion=1 契约由 Kernel 维护,前端按 schemaVersion 解析。
export interface SensorEntry {
id: string
name: string
/** 传感器类型(LHB SensorType 小写):temperature/load/power/voltage/fan/clock/data/smalldata/throughput/level/control 等 */
type: string
hardwareName: string
/** null 表示首轮未就绪或硬件不可读 */
value: number | null
unit: string
}
export interface SensorGroup {
id: string
name: string
sensors: SensorEntry[]
}
export interface SensorSnapshot {
schemaVersion: number
timestamp: number
/** 仅首个快照有意义,后续为 0 */
coldStartMs?: number
isAdmin: boolean
ready: boolean
groups: SensorGroup[]
}
export interface MonitorStatus {
running: boolean
pid: number | null
ready: boolean
sensorCount: number
restartCount: number
}
export interface MonitorKernelInfo {
path: string
exists: boolean
port: number
}
/** 连接状态机:与后端事件一一对应 */
export type ConnectionState = 'idle' | 'loading' | 'connected' | 'disconnected' | 'error'
/** 5 秒未收到 monitor-data 事件视为掉线(与后端心跳节奏一致) */
const STALE_TIMEOUT_MS = 5000
export const useMonitorStore = defineStore('monitor', () => {
// ===== state =====
const status = ref<MonitorStatus | null>(null)
const snapshot = ref<SensorSnapshot | null>(null)
const kernelInfo = ref<MonitorKernelInfo | null>(null)
const errorMsg = ref<string | null>(null)
/** 累计收到的 monitor-data 事件数,用于诊断与"已连接"判定 */
const eventCount = ref(0)
/** 最近一次收到 monitor-data 的时间戳(ms */
const lastEventTime = ref(0)
/** 是否正在启动 / 停止 Kernel(防止重复点击) */
const starting = ref(false)
const stopping = ref(false)
/** 是否已完成首次加载(避免初始 null/false 导致 UI 闪烁误导状态) */
const initialized = ref(false)
let unlistenFns: UnlistenFn[] = []
// ===== getters =====
/** 当前连接状态(基于 status + 最近事件时间推断) */
const connState = computed<ConnectionState>(() => {
if (errorMsg.value) return 'error'
if (!status.value) return 'idle'
if (!status.value.running) return 'idle'
if (!status.value.ready) return 'loading'
if (eventCount.value === 0) return 'loading'
if (Date.now() - lastEventTime.value > STALE_TIMEOUT_MS) return 'disconnected'
return 'connected'
})
/** 是否处于"已就绪 + 收到数据"的健康状态 */
const isLive = computed(() => connState.value === 'connected')
/** 按分组 id 查找快照 */
const groupById = computed(() => {
const map: Record<string, SensorGroup> = {}
for (const g of snapshot.value?.groups ?? []) map[g.id] = g
return map
})
/**
* 在指定分组下查找首个匹配的传感器值。
* @param groupId 分组 idcpu/memory/gpuintel/storage 等)
* @param matcher 传感器名匹配(精确或子串)
*/
function findSensorValue(groupId: string, matcher: { name?: string; hardwareName?: string; type?: string }): number | null {
const g = groupById.value[groupId]
if (!g) return null
const s = g.sensors.find(s =>
(!matcher.name || s.name === matcher.name || s.name.includes(matcher.name)) &&
(!matcher.hardwareName || s.hardwareName === matcher.hardwareName || s.hardwareName.includes(matcher.hardwareName)) &&
(!matcher.type || s.type === matcher.type)
)
return s?.value ?? null
}
// ===== actions =====
async function refreshStatus() {
try {
status.value = await invoke<MonitorStatus>('monitor_status')
errorMsg.value = null
} catch (e) {
logger.error('获取状态失败: ' + e)
} finally {
initialized.value = true
}
return status.value
}
async function refreshKernelInfo() {
try {
kernelInfo.value = await invoke<MonitorKernelInfo>('monitor_kernel_info')
} catch (e) {
logger.error('获取 Kernel 信息失败: ' + e)
}
return kernelInfo.value
}
async function start() {
if (starting.value) return
starting.value = true
errorMsg.value = null
try {
await invoke('monitor_start')
await refreshStatus()
} catch (e) {
errorMsg.value = String(e)
logger.error('启动失败: ' + e)
} finally {
starting.value = false
}
}
async function stop() {
if (stopping.value) return
stopping.value = true
try {
await invoke('monitor_stop')
snapshot.value = null
eventCount.value = 0
lastEventTime.value = 0
await refreshStatus()
} catch (e) {
errorMsg.value = String(e)
logger.error('停止失败: ' + e)
} finally {
stopping.value = false
}
}
/** 主动拉取一次性快照(切回 tab 时立即填充,不等下一个 SSE tick) */
async function fetchSnapshot() {
try {
snapshot.value = await invoke<SensorSnapshot>('monitor_get_snapshot')
} catch (e) {
logger.error('拉取快照失败: ' + e)
}
return snapshot.value
}
/** 订阅 Tauri 事件:monitor-data / monitor-ready / monitor-loading / monitor-disconnected / monitor-error */
async function subscribe() {
if (unlistenFns.length) return
unlistenFns.push(await listen<SensorSnapshot>('monitor-data', (e) => {
// schemaVersion 守卫:仅接受 v1,未来版本需在此处显式升级
if (e.payload?.schemaVersion !== 1) {
logger.warn('收到未知 schemaVersion: ' + e.payload?.schemaVersion)
return
}
snapshot.value = e.payload
eventCount.value++
lastEventTime.value = Date.now()
}))
unlistenFns.push(await listen('monitor-ready', () => {
refreshStatus()
}))
unlistenFns.push(await listen('monitor-loading', () => {
// 状态由 status 轮询反映
}))
unlistenFns.push(await listen('monitor-disconnected', () => {
logger.warn('SSE 断开,等待自动重连')
refreshStatus()
}))
unlistenFns.push(await listen<{ message?: string }>('monitor-error', (e) => {
errorMsg.value = e.payload?.message ?? 'Kernel 错误'
logger.error('Kernel 错误: ' + JSON.stringify(e.payload))
}))
}
function unsubscribe() {
unlistenFns.forEach(fn => fn())
unlistenFns = []
}
/** 模块挂载时调用:刷新状态 + 订阅事件 + 拉取一次快照 */
async function init() {
await Promise.all([refreshStatus(), refreshKernelInfo()])
await subscribe()
// 若 Kernel 已就绪,立即拉一次快照避免 UI 空白
if (status.value?.ready) {
await fetchSnapshot()
}
}
/** 模块卸载时调用:仅取消事件订阅,不停止 KernelKernel 由 ProcessManager 全局管理) */
function dispose() {
unsubscribe()
}
return {
// state
status,
snapshot,
kernelInfo,
errorMsg,
eventCount,
lastEventTime,
starting,
stopping,
initialized,
// getters
connState,
isLive,
groupById,
// actions
findSensorValue,
refreshStatus,
refreshKernelInfo,
start,
stop,
fetchSnapshot,
init,
dispose,
}
})