-
-
硬件监控功能开发中...
-
支持 CPU、GPU、内存、硬盘等硬件数据监控
+
+
+
+
+
+
+
+
+
+
+
+
+
Kernel 未启动
+
+
+
+
+
+
+
+
+ Kernel 状态
+
+ {{ stateMeta[store.connState].text }}
+
+
+
+
+
+
+ 进程
+ {{ store.status?.running ? '运行中' : '已停止' }}
+
+
+ PID
+ {{ store.status?.pid ?? '--' }}
+
+
+ 就绪
+ {{ store.status?.ready ? '是' : '否' }}
+
+
+ 传感器数
+ {{ store.status?.sensorCount ?? '--' }}
+
+
+ 重启次数
+ {{ store.status?.restartCount ?? 0 }}
+
+
+ 事件数
+ {{ store.eventCount }}
+
+
+
+
+
+
+ 权限
+
+
+ {{ store.snapshot?.isAdmin ? '管理员' : '普通' }}
+
+
+
+
+
非管理员运行,部分传感器(电压/主板/SMBus)可能不可读。可在设置页查看降级说明。
+
+
+
+
+
+
+
+
+
+
+
+
+ CPU
+
+
+
+
+
+
封装温度
+
+ {{ fmt(cpuTemp, 0) }}°C
+
+
+
+
+
+
+ 总负载
+ {{ fmt(cpuLoad, 0) }}%
+
+
+
+
+
+
+
+
+
+
+ 内存
+
+
+
+
+
+ 内存负载
+ {{ fmt(memLoad, 0) }}%
+
+
+
+
+
+
+
+
+
+
+
+ GPU
+
+
+
+
+
+ 3D 负载
+ {{ fmt(gpuLoad, 0) }}%
+
+
+
+ 暂无 GPU 数据
+
+
+
+
+
+
+
+ 存储
+
+
+
+ 温度
+
+ {{ fmt(storageTemp, 0) }}°C
+
+
+
+
+
+
+
+
+
+
SSE 连接断开
+
Kernel 可能正在重启,等待自动重连...
+
+
+
+
+
+
+
+
+
+
错误
+
{{ store.errorMsg }}
+
+
+
+
+
+
+
+
+
+
+
+
无快照数据
+
请先在概览页启动 Kernel
+
-
-
+
+
+
+
{{ groups.length }} 个分组 / {{ store.status?.sensorCount ?? 0 }} 个传感器
+
+
+
+
+
+
+
+
+
+
+ {{ groupDisplayName(g.id, g.name) }}
+ {{ g.sensors.length }}
+
+ {{ g.id }}
+
+
+
+
+
+ {{ hwGroup.hardware }}
+
+
+
{{ typeLabel(typeGroup.type) }}
+
+
+ {{ s.name }}
+
+ {{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }}
+ {{ s.unit }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Kernel 控制
+
+
+
+
+
+
+
+
+
+ Kernel 由 ProcessManager 统一管理,应用启动时自动拉起,崩溃自动重启。
+
+
+
+
+
+
+
+
+ Kernel 信息
+
+
+
+
+ 端口
+ {{ store.kernelInfo.port }}
+
+
+ 已安装
+
+ {{ store.kernelInfo.exists ? '是' : '否' }}
+
+
+
+ 路径
+ {{ store.kernelInfo.path }}
+
+
+
+
+
+
+
+
+ 权限与降级
+
+
+
+ 当前权限:{{ store.snapshot?.isAdmin ? '管理员' : '普通' }}
+
+
+ LibreHardwareMonitor 访问 SMBus、部分 EC 传感器、某些 GPU 传感器需要管理员权限。
+ 默认非提权运行,覆盖大部分 CPU/GPU 温度(通过 OHM RPC/WMI 仍可读),牺牲部分主板/电压传感器。
+
+
+
+ 提权策略(任务计划程序免 UAC 启动)将在阶段五实现,当前以普通权限运行。
+
+
+
+
+
+
+
+
+ 关于
+
+
+
+
+ 数据源:LibreHardwareMonitorLib 0.9.5(Native AOT 编译,独立进程)
+
+
+ 通信:HTTP + SSE(本地 loopback),schemaVersion=1 数据契约
+
+
+ 冷启动耗时:{{ (store.snapshot.coldStartMs / 1000).toFixed(1) }} 秒
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/modules/monitor/index.ts b/src/modules/monitor/index.ts
index 4cc3fcb..28b5330 100644
--- a/src/modules/monitor/index.ts
+++ b/src/modules/monitor/index.ts
@@ -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
}
diff --git a/src/stores/monitorStore.ts b/src/stores/monitorStore.ts
new file mode 100644
index 0000000..8eb3587
--- /dev/null
+++ b/src/stores/monitorStore.ts
@@ -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
(null)
+ const snapshot = ref(null)
+ const kernelInfo = ref(null)
+ const errorMsg = ref(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(() => {
+ 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 = {}
+ for (const g of snapshot.value?.groups ?? []) map[g.id] = g
+ return map
+ })
+
+ /**
+ * 在指定分组下查找首个匹配的传感器值。
+ * @param groupId 分组 id(cpu/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('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('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('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('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()
+ }
+ }
+
+ /** 模块卸载时调用:仅取消事件订阅,不停止 Kernel(Kernel 由 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,
+ }
+})