监控模块 优化
This commit is contained in:
@@ -2,10 +2,13 @@
|
||||
import {
|
||||
Activity, Play, Square, RefreshCw, Loader2, Cpu, MemoryStick,
|
||||
Gauge, HardDrive, Settings as SettingsIcon, AlertTriangle,
|
||||
ShieldCheck, ShieldAlert, Zap, Thermometer, Clock, ChevronDown,
|
||||
ShieldCheck, ShieldOff, Zap, Thermometer, Clock, ChevronDown,
|
||||
KeyRound, ArrowDown, ArrowUp, Wifi, FolderOpen, Copy, ListChecks,
|
||||
} from '@lucide/vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { appDataDir } from '@tauri-apps/api/path'
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
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'
|
||||
@@ -16,6 +19,9 @@ 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'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
|
||||
const store = useMonitorStore()
|
||||
|
||||
@@ -31,9 +37,9 @@ const tabsListRef = useModuleTabs(activeTab, [
|
||||
// 容量 30 点,约对应 30 秒(fast 通道 1s 推送一次)
|
||||
const HISTORY_LEN = 30
|
||||
const cpuTempHistory = ref<number[]>([])
|
||||
const cpuLoadHistory = ref<number[]>([])
|
||||
const memLoadHistory = ref<number[]>([])
|
||||
const gpuLoadHistory = ref<number[]>([])
|
||||
const gpuTempHistory = ref<number[]>([])
|
||||
const netDownHistory = ref<number[]>([])
|
||||
const netUpHistory = ref<number[]>([])
|
||||
|
||||
function pushHistory(buf: typeof cpuTempHistory, v: number | null) {
|
||||
if (v == null || !isFinite(v)) return
|
||||
@@ -41,31 +47,112 @@ function pushHistory(buf: typeof cpuTempHistory, v: number | null) {
|
||||
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 =====
|
||||
|
||||
/** GPU 分组 id(兼容 intel/amd/nvidia) */
|
||||
const gpuGroupId = computed(() => {
|
||||
if (store.groupById['gpuintel']) return 'gpuintel'
|
||||
if (store.groupById['gpuamd']) return 'gpuamd'
|
||||
if (store.groupById['gpunvidia']) return 'gpunvidia'
|
||||
return null
|
||||
})
|
||||
|
||||
const cpuModel = computed(() => store.groupById['cpu']?.sensors[0]?.hardwareName ?? null)
|
||||
const cpuTemp = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'temperature' }))
|
||||
const cpuLoad = computed(() => store.findSensorValue('cpu', { name: 'CPU Total', type: 'load' }))
|
||||
const cpuPower = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'power' }))
|
||||
|
||||
const gpuModel = computed(() => {
|
||||
const gid = gpuGroupId.value
|
||||
return gid ? store.groupById[gid]?.sensors[0]?.hardwareName ?? null : null
|
||||
})
|
||||
const gpuTemp = computed(() => {
|
||||
const gid = gpuGroupId.value
|
||||
if (!gid) return null
|
||||
// GPU 温度传感器命名因厂商而异,按优先级匹配
|
||||
return store.findSensorValue(gid, { name: 'GPU Core', type: 'temperature' })
|
||||
?? store.findSensorValue(gid, { name: 'GPU Temperature', type: 'temperature' })
|
||||
?? store.findSensorValue(gid, { type: 'temperature' })
|
||||
})
|
||||
const gpuLoad = computed(() => {
|
||||
const gid = gpuGroupId.value
|
||||
if (!gid) return null
|
||||
return store.findSensorValue(gid, { name: 'D3D 3D', type: 'load' })
|
||||
?? store.findSensorValue(gid, { name: 'GPU Core', type: 'load' })
|
||||
?? store.findSensorValue(gid, { type: 'load' })
|
||||
})
|
||||
const gpuPower = computed(() => {
|
||||
const gid = gpuGroupId.value
|
||||
if (!gid) return null
|
||||
return store.findSensorValue(gid, { name: 'GPU Power', type: 'power' })
|
||||
?? store.findSensorValue(gid, { type: 'power' })
|
||||
})
|
||||
|
||||
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' }))
|
||||
const memUsedGB = computed(() => store.findSensorValue('memory', { hardwareName: 'Total Memory', name: 'Memory Used', type: 'data' }))
|
||||
const memAvailGB = computed(() => store.findSensorValue('memory', { hardwareName: 'Total Memory', name: 'Memory Available', type: 'data' }))
|
||||
const memTotalGB = computed(() => {
|
||||
const used = memUsedGB.value
|
||||
const avail = memAvailGB.value
|
||||
if (used != null && avail != null) return used + avail
|
||||
return null
|
||||
})
|
||||
/** 物理内存条型号列表(排除 Virtual Memory / Total Memory 虚拟分组) */
|
||||
const memModuleModels = computed(() => {
|
||||
const g = store.groupById['memory']
|
||||
if (!g) return []
|
||||
const models = new Set<string>()
|
||||
for (const s of g.sensors) {
|
||||
if (s.hardwareName !== 'Virtual Memory' && s.hardwareName !== 'Total Memory') {
|
||||
models.add(s.hardwareName)
|
||||
}
|
||||
}
|
||||
return Array.from(models)
|
||||
})
|
||||
|
||||
/** 存储硬盘列表(按 hardwareName 分组,提取温度/使用率/容量) */
|
||||
interface StorageDrive {
|
||||
name: string
|
||||
temp: number | null
|
||||
usedPct: number | null
|
||||
totalGB: number | null
|
||||
usedGB: number | null
|
||||
}
|
||||
const storageDrives = computed<StorageDrive[]>(() => {
|
||||
const g = store.groupById['storage']
|
||||
if (!g) return []
|
||||
const byHw = new Map<string, SensorEntry[]>()
|
||||
for (const s of g.sensors) {
|
||||
if (!byHw.has(s.hardwareName)) byHw.set(s.hardwareName, [])
|
||||
byHw.get(s.hardwareName)!.push(s)
|
||||
}
|
||||
return Array.from(byHw.entries()).map(([hw, sensors]) => {
|
||||
const temp = sensors.find(s => s.type === 'temperature')?.value ?? null
|
||||
const usedPct = sensors.find(s => s.name === 'Used Space')?.value ?? null
|
||||
const totalGB = sensors.find(s => s.name === 'Total Space')?.value ?? null
|
||||
const freeGB = sensors.find(s => s.name === 'Free Space')?.value ?? null
|
||||
const usedGB = totalGB != null && freeGB != null ? totalGB - freeGB : null
|
||||
return { name: hw, temp, usedPct, totalGB, usedGB }
|
||||
})
|
||||
})
|
||||
|
||||
// 监听快照变化,提取温度历史(用于 CPU/GPU 温度 sparkline)
|
||||
watch(() => store.snapshot, () => {
|
||||
pushHistory(cpuTempHistory, cpuTemp.value)
|
||||
pushHistory(gpuTempHistory, gpuTemp.value)
|
||||
}, { deep: false })
|
||||
|
||||
// 监听网速事件,记录历史(用于网速 sparkline)
|
||||
watch(() => store.networkSpeed, (v) => {
|
||||
if (v) {
|
||||
pushHistory(netDownHistory, v.downloadBps)
|
||||
pushHistory(netUpHistory, v.uploadBps)
|
||||
}
|
||||
}, { deep: false })
|
||||
|
||||
// 网速格式化(computed 避免模板中重复调用)
|
||||
const downSpeed = computed(() => fmtSpeed(store.networkSpeed?.downloadBps ?? null))
|
||||
const upSpeed = computed(() => fmtSpeed(store.networkSpeed?.uploadBps ?? null))
|
||||
|
||||
// ===== 工具函数 =====
|
||||
|
||||
@@ -92,6 +179,14 @@ function loadColor(v: number | null): string {
|
||||
return 'text-red-500'
|
||||
}
|
||||
|
||||
/** 格式化网速(bytes/s → 自适应 KB/s 或 MB/s) */
|
||||
function fmtSpeed(bytesPerSec: number | null): { value: string; unit: string } {
|
||||
if (bytesPerSec == null || !isFinite(bytesPerSec)) return { value: '--', unit: '' }
|
||||
if (bytesPerSec >= 1_048_576) return { value: (bytesPerSec / 1_048_576).toFixed(2), unit: 'MB/s' }
|
||||
if (bytesPerSec >= 1024) return { value: (bytesPerSec / 1024).toFixed(1), unit: 'KB/s' }
|
||||
return { value: bytesPerSec.toFixed(0), unit: 'B/s' }
|
||||
}
|
||||
|
||||
/** 传感器类型 → 中文标签 */
|
||||
const typeLabels: Record<string, string> = {
|
||||
temperature: '温度',
|
||||
@@ -214,8 +309,151 @@ async function handleRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 提权启动 Kernel(弹 UAC)。
|
||||
* 普通权限下 CPU 温度/时钟/存储等传感器不可读,提权后可获取完整数据。
|
||||
*/
|
||||
async function handleStartElevated() {
|
||||
await store.startElevated()
|
||||
if (store.status?.elevated) {
|
||||
toast.success('Kernel 已提权运行')
|
||||
} else if (store.errorMsg) {
|
||||
toast.error('提权失败', { description: store.errorMsg })
|
||||
}
|
||||
}
|
||||
|
||||
/** 永久提权:以管理员权限重启 Thing 自身,并持久化标志使后续启动自动提权。
|
||||
* 非管理员时进程退出;已是管理员时仅设置标志并返回。
|
||||
*/
|
||||
async function handleElevateSelf() {
|
||||
await store.elevateSelf()
|
||||
// 非管理员时进程已退出,不会走到这里
|
||||
if (store.errorMsg) {
|
||||
toast.error('永久提权失败', { description: store.errorMsg })
|
||||
} else if (store.elevateOnLaunch) {
|
||||
toast.success('已启用永久提权', { description: '后续启动将自动以管理员权限运行' })
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消永久提权:清除标志,下次启动不再触发 UAC(当前会话权限不变) */
|
||||
async function handleCancelElevation() {
|
||||
await store.cancelElevation()
|
||||
if (!store.errorMsg) {
|
||||
toast.success('已取消永久提权', { description: '下次启动将以普通权限运行' })
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Kernel 路径显示 + 复制 + 打开文件夹(参考代理模块 mihomo 信息板块) =====
|
||||
const appDataPath = ref('')
|
||||
/** 路径缩略显示:只显示最后 3 段 */
|
||||
const pathShort = computed(() => {
|
||||
const p = store.kernelInfo?.path
|
||||
if (!p) return '—'
|
||||
const parts = p.replace(/\\/g, '/').split('/')
|
||||
if (parts.length <= 4) return p
|
||||
return '.../' + parts.slice(-3).join('/')
|
||||
})
|
||||
/** 路径完整显示:将 appData 部分替换为 %APPDATA% 便于在文件管理器地址栏打开 */
|
||||
const pathDisplay = computed(() => {
|
||||
const p = store.kernelInfo?.path
|
||||
if (!p) return ''
|
||||
if (appDataPath.value && p.toLowerCase().startsWith(appDataPath.value.toLowerCase())) {
|
||||
return '%APPDATA%' + p.slice(appDataPath.value.length)
|
||||
}
|
||||
return p
|
||||
})
|
||||
async function copyPath() {
|
||||
const p = store.kernelInfo?.path
|
||||
if (!p) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(p)
|
||||
toast.success('路径已复制', { description: pathDisplay.value })
|
||||
} catch {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
}
|
||||
async function openFolder() {
|
||||
const p = store.kernelInfo?.path
|
||||
if (!p) return
|
||||
try {
|
||||
await revealItemInDir(p)
|
||||
} catch (e) {
|
||||
toast.error('打开文件夹失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 硬件监控配置 Dialog =====
|
||||
const configDialogOpen = ref(false)
|
||||
/** Dialog 内编辑中的硬件开关(key → bool) */
|
||||
const editingHardware = ref<Record<string, boolean>>({})
|
||||
/** Dialog 内编辑中的传感器类型开关(key → bool) */
|
||||
const editingSensorTypes = ref<Record<string, boolean>>({})
|
||||
const savingConfig = ref(false)
|
||||
|
||||
async function openConfigDialog() {
|
||||
await store.fetchHardwareConfig()
|
||||
if (store.hardwareConfig) {
|
||||
// 深拷贝当前配置到编辑状态
|
||||
editingHardware.value = { ...store.hardwareConfig.config.hardware }
|
||||
editingSensorTypes.value = { ...store.hardwareConfig.config.sensorTypes }
|
||||
}
|
||||
configDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleSaveConfig() {
|
||||
savingConfig.value = true
|
||||
try {
|
||||
const resp = await store.saveHardwareConfig(editingHardware.value, editingSensorTypes.value)
|
||||
if (resp?.success) {
|
||||
if (resp.restartRequired) {
|
||||
// 检测是否启用了需要管理员权限的硬件但当前非管理员
|
||||
const needsAdmin = store.hardwareConfig?.availableHardware.some(
|
||||
hw => editingHardware.value[hw.key] && hw.requiresAdmin
|
||||
) ?? false
|
||||
const isElevated = store.status?.elevated || store.status?.thingElevated
|
||||
|
||||
configDialogOpen.value = false
|
||||
|
||||
if (needsAdmin && !isElevated) {
|
||||
toast.warning('部分硬件需要管理员权限', {
|
||||
description: '主板/存储等硬件需提权才能读取完整数据,建议永久提权',
|
||||
duration: 6000,
|
||||
})
|
||||
} else {
|
||||
toast.success('配置已保存', { description: '正在重启 Kernel...' })
|
||||
}
|
||||
|
||||
// 自动重启 Kernel 以应用硬件开关变更
|
||||
await store.stop()
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
await store.start()
|
||||
|
||||
// 等待 Kernel 就绪后拉取快照(cold start 可能需要数秒)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
await store.refreshStatus()
|
||||
if (store.status?.ready) {
|
||||
await store.fetchSnapshot()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.success('配置已保存', { description: '传感器类型过滤已热生效' })
|
||||
configDialogOpen.value = false
|
||||
// 热更新后立即拉取新快照以反映传感器类型过滤变化
|
||||
await store.fetchSnapshot()
|
||||
}
|
||||
} else if (store.errorMsg) {
|
||||
toast.error('保存失败', { description: store.errorMsg })
|
||||
}
|
||||
} finally {
|
||||
savingConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 获取 appData 路径,用于将 Kernel 路径替换为 %APPDATA% 形式
|
||||
try { appDataPath.value = await appDataDir() } catch { /* 忽略 */ }
|
||||
store.init()
|
||||
})
|
||||
|
||||
@@ -245,105 +483,42 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
<!-- ===== 概览 ===== -->
|
||||
<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>
|
||||
<!-- 始终显示卡片网格,未启动时数据以占位符显示,保持画面完整 -->
|
||||
<div v-else key="content" class="grid grid-cols-1 md:grid-cols-3 gap-2.5">
|
||||
<!-- CPU(温度 + 功耗 + 曲线图,未读数据以 -- 占位) -->
|
||||
<Card class="py-0 gap-0">
|
||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||
<CardTitle class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center gap-1.5"><Cpu class="size-4 text-primary" />CPU</span>
|
||||
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="cpuModel ?? ''">{{ cpuModel ?? '--' }}</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">
|
||||
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
||||
<!-- 温度 + 功耗 + 曲线图 -->
|
||||
<div class="flex items-end justify-between gap-2">
|
||||
<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)]">
|
||||
<div :class="['text-2xl font-bold tabular-nums leading-tight', 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" />
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1 justify-end"><Zap class="size-3" />功耗</div>
|
||||
<div class="text-base font-medium tabular-nums">{{ fmt(cpuPower, 1) }}<span class="text-xs text-muted-foreground ml-0.5">W</span></div>
|
||||
</div>
|
||||
<svg v-if="cpuTempHistory.length >= 2" :width="56" :height="24" class="opacity-70 shrink-0">
|
||||
<path :d="sparklinePath(cpuTempHistory, { w: 56, h: 24 })" 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">
|
||||
<div class="flex items-center justify-between text-xs mb-0.5">
|
||||
<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>
|
||||
@@ -352,64 +527,203 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 内存 -->
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<MemoryStick class="size-4 text-primary" />内存
|
||||
<!-- GPU(结构与 CPU 一致:温度 + 功耗 + 曲线图,未读数据以 -- 占位) -->
|
||||
<Card class="py-0 gap-0">
|
||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||
<CardTitle class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center gap-1.5"><Gauge class="size-4 text-primary" />GPU</span>
|
||||
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="gpuModel ?? ''">{{ gpuModel ?? '--' }}</span>
|
||||
</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>
|
||||
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
||||
<!-- 温度 + 功耗 + 曲线图(与 CPU 卡片结构一致) -->
|
||||
<div class="flex items-end justify-between gap-2">
|
||||
<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 leading-tight', tempColor(gpuTemp)]">
|
||||
{{ fmt(gpuTemp, 0) }}<span class="text-sm font-normal">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
<Progress :model-value="memLoad ?? 0" class="h-1.5" />
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1 justify-end"><Zap class="size-3" />功耗</div>
|
||||
<div class="text-base font-medium tabular-nums">{{ fmt(gpuPower, 1) }}<span class="text-xs text-muted-foreground ml-0.5">W</span></div>
|
||||
</div>
|
||||
<svg v-if="gpuTempHistory.length >= 2" :width="56" :height="24" class="opacity-70 shrink-0">
|
||||
<path :d="sparklinePath(gpuTempHistory, { w: 56, h: 24 })" stroke="currentColor" :class="tempColor(gpuTemp)" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</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>
|
||||
<!-- 负载 -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-0.5">
|
||||
<span class="text-muted-foreground flex items-center gap-1"><Gauge class="size-3" />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">
|
||||
<!-- 内存(主指标大字 + 进度条,未读数据以 -- 占位) -->
|
||||
<Card class="py-0 gap-0">
|
||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||
<CardTitle class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center gap-1.5"><MemoryStick class="size-4 text-primary" />内存</span>
|
||||
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="memModuleModels.join(', ')">
|
||||
{{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
||||
<!-- 已使用 / 总容量(主指标,与 CPU 温度对齐) -->
|
||||
<div class="flex items-end justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1"><MemoryStick class="size-3" />已使用</div>
|
||||
<div class="text-2xl font-bold tabular-nums leading-tight">
|
||||
{{ fmt(memUsedGB, 1) }}<span class="text-sm font-normal text-muted-foreground"> / {{ fmt(memTotalGB, 1) }} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 负载进度条(与 CPU 负载对齐) -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-0.5">
|
||||
<span class="text-muted-foreground flex items-center gap-1"><Gauge class="size-3" />负载</span>
|
||||
<span :class="['font-medium tabular-nums', loadColor(memLoad)]">{{ fmt(memLoad, 0) }}%</span>
|
||||
</div>
|
||||
<Progress :model-value="memLoad ?? 0" class="h-1.5" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 网络(跨3列,下载/上传 + 曲线图,独立于 Kernel 由 Tauri 后台推送) -->
|
||||
<Card class="md:col-span-3 py-0 gap-0">
|
||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||
<CardTitle class="flex items-center gap-1.5 text-sm">
|
||||
<Wifi class="size-4 text-primary" />网络
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="px-3.5 pb-2.5">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- 下载 -->
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1 mb-0.5"><ArrowDown class="size-3 text-sky-500" />下载</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="text-2xl font-bold tabular-nums leading-tight text-sky-500 shrink-0">
|
||||
{{ downSpeed.value }}<span class="text-sm font-normal text-muted-foreground ml-0.5">{{ downSpeed.unit }}</span>
|
||||
</div>
|
||||
<svg v-if="netDownHistory.length >= 2" class="flex-1 h-7 opacity-70" viewBox="0 0 80 28" preserveAspectRatio="none">
|
||||
<path :d="sparklinePath(netDownHistory, { w: 80, h: 28 })" stroke="currentColor" class="text-sky-500" fill="none" stroke-width="1.5" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传 -->
|
||||
<div>
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-1 mb-0.5"><ArrowUp class="size-3 text-violet-500" />上传</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="text-2xl font-bold tabular-nums leading-tight text-violet-500 shrink-0">
|
||||
{{ upSpeed.value }}<span class="text-sm font-normal text-muted-foreground ml-0.5">{{ upSpeed.unit }}</span>
|
||||
</div>
|
||||
<svg v-if="netUpHistory.length >= 2" class="flex-1 h-7 opacity-70" viewBox="0 0 80 28" preserveAspectRatio="none">
|
||||
<path :d="sparklinePath(netUpHistory, { w: 80, h: 28 })" stroke="currentColor" class="text-violet-500" fill="none" stroke-width="1.5" vector-effect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 存储(跨3列):列出各硬盘温度/容量/使用率,未读到以占位符显示 -->
|
||||
<Card class="md:col-span-3 py-0 gap-0">
|
||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||
<CardTitle class="flex items-center gap-1.5 text-sm">
|
||||
<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>
|
||||
<CardContent class="px-3.5 pb-2.5">
|
||||
<div v-if="storageDrives.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<div v-for="drive in storageDrives" :key="drive.name" class="border rounded-md p-2 space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs font-medium truncate" :title="drive.name">{{ drive.name }}</span>
|
||||
<span :class="['text-xs font-mono tabular-nums shrink-0', tempColor(drive.temp)]">{{ fmt(drive.temp, 0) }}°C</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-0.5">
|
||||
<span class="text-muted-foreground">使用率</span>
|
||||
<span class="font-mono tabular-nums">{{ fmt(drive.usedPct, 0) }}%</span>
|
||||
</div>
|
||||
<Progress :model-value="drive.usedPct ?? 0" class="h-1" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>容量</span>
|
||||
<span class="font-mono tabular-nums">{{ fmt(drive.usedGB, 1) }} / {{ fmt(drive.totalGB, 1) }} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无数据占位(保持卡片结构完整) -->
|
||||
<div v-else class="text-xs text-muted-foreground py-2 text-center">暂无存储数据</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Kernel 状态卡片(跨3列,含启动/停止/刷新/提权按钮) -->
|
||||
<Card class="md:col-span-3 py-0 gap-0">
|
||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||
<CardTitle class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center gap-1.5"><Activity class="size-4 text-primary" />Kernel 状态</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span :class="['text-xs px-2 py-0.5 rounded-full', stateMeta[store.connState].class]">
|
||||
{{ stateMeta[store.connState].text }}
|
||||
</span>
|
||||
<Badge :variant="store.snapshot?.isAdmin ? 'default' : 'outline'" :class="store.snapshot?.isAdmin ? 'bg-emerald-500 hover:bg-emerald-500' : ''">
|
||||
{{ store.snapshot?.isAdmin ? '管理员' : '普通' }}
|
||||
</Badge>
|
||||
<Badge v-if="store.status?.thingElevated" variant="outline" class="text-xs border-emerald-500/50 text-emerald-600 dark:text-emerald-400">
|
||||
<ShieldCheck class="size-2.5 mr-0.5" />永久提权
|
||||
</Badge>
|
||||
<Badge v-else-if="store.status?.elevated" variant="outline" class="text-xs border-violet-500/50 text-violet-600 dark:text-violet-400">
|
||||
<KeyRound class="size-2.5 mr-0.5" />提权
|
||||
</Badge>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="px-3.5 pb-2.5">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs">
|
||||
<span class="text-muted-foreground">PID: <span class="font-mono text-foreground">{{ store.status?.pid ?? '--' }}</span></span>
|
||||
<span class="text-muted-foreground">传感器: <span class="font-mono text-foreground">{{ store.status?.sensorCount ?? '--' }}</span></span>
|
||||
<span class="text-muted-foreground">重启: <span class="font-mono text-foreground">{{ store.status?.restartCount ?? 0 }}</span></span>
|
||||
<span class="text-muted-foreground">事件: <span class="font-mono text-foreground">{{ store.eventCount }}</span></span>
|
||||
<div class="flex items-center gap-1.5 ml-auto">
|
||||
<!-- 启动按钮(未运行时显示) -->
|
||||
<Button v-if="store.connState === 'idle'" size="xs" :disabled="store.starting" @click="handleStart">
|
||||
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
|
||||
<Play v-else class="size-3" />启动
|
||||
</Button>
|
||||
<!-- 普通提权按钮(仅提权 ThingHK,Thing 非管理员且 ThingHK 未提权时显示) -->
|
||||
<Button v-if="!store.snapshot?.isAdmin && !store.status?.elevated && !store.status?.thingElevated" size="xs" variant="outline" class="gap-1 text-orange-600 dark:text-orange-400 border-orange-500/40 hover:bg-orange-500/10" :disabled="store.starting" title="以管理员权限启动 Kernel(弹 UAC,仅提权 ThingHK)" @click="handleStartElevated">
|
||||
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
|
||||
<KeyRound v-else class="size-3" />提权
|
||||
</Button>
|
||||
<!-- 永久提权按钮(标志未启用时显示:设置标志 + 以管理员权限重启 Thing) -->
|
||||
<Button v-if="!store.elevateOnLaunch" size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" title="以管理员权限重启 Thing(弹 UAC,ThingHK 子进程继承权限,后续启动自动提权)" @click="handleElevateSelf">
|
||||
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
|
||||
<ShieldCheck v-else class="size-3" />永久提权
|
||||
</Button>
|
||||
<!-- 取消永久提权按钮(标志已启用时显示:清除标志,下次启动不触发 UAC) -->
|
||||
<Button v-else size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" title="取消永久提权,下次启动将以普通权限运行(不影响当前会话)" @click="handleCancelElevation">
|
||||
<ShieldOff class="size-3" />取消提权
|
||||
</Button>
|
||||
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleRefresh">
|
||||
<RefreshCw class="size-3" />刷新
|
||||
</Button>
|
||||
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleStop">
|
||||
<Square class="size-3" />停止
|
||||
</Button>
|
||||
</div>
|
||||
</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">
|
||||
<Card v-if="store.connState === 'disconnected'" class="md:col-span-3 border-orange-500/40 py-0 gap-0">
|
||||
<CardContent class="pt-3 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>
|
||||
@@ -419,8 +733,8 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
</Card>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<Card v-if="store.errorMsg" class="border-red-500/40">
|
||||
<CardContent class="pt-4 flex items-start gap-2 text-sm">
|
||||
<Card v-if="store.errorMsg" class="md:col-span-3 border-red-500/40 py-0 gap-0">
|
||||
<CardContent class="pt-3 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>
|
||||
@@ -513,6 +827,9 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
<Button size="sm" variant="outline" :disabled="!store.status?.ready" @click="handleRefresh">
|
||||
<RefreshCw class="size-3.5" />刷新状态
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" :disabled="!store.status?.ready" @click="openConfigDialog">
|
||||
<ListChecks class="size-3.5" />监控项
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
Kernel 由 ProcessManager 统一管理,应用启动时自动拉起,崩溃自动重启。
|
||||
@@ -538,9 +855,38 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
{{ store.kernelInfo.exists ? '是' : '否' }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<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 class="flex items-center gap-1.5 min-w-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span
|
||||
v-if="store.kernelInfo.exists"
|
||||
class="font-mono text-xs text-right truncate cursor-default"
|
||||
>{{ pathDisplay || pathShort }}</span>
|
||||
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-[400px] break-all">{{ store.kernelInfo.path ?? '' }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<template v-if="store.kernelInfo.exists">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon-xs" variant="ghost" @click="copyPath">
|
||||
<Copy class="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>复制路径</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon-xs" variant="ghost" @click="openFolder">
|
||||
<FolderOpen class="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>在文件夹中显示</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -552,16 +898,48 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
<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>
|
||||
<CardContent class="space-y-3 text-sm text-muted-foreground">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>当前权限</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Badge :variant="store.snapshot?.isAdmin ? 'default' : 'outline'" :class="store.snapshot?.isAdmin ? 'bg-emerald-500 hover:bg-emerald-500' : ''">
|
||||
{{ store.snapshot?.isAdmin ? '管理员' : '普通' }}
|
||||
</Badge>
|
||||
<Badge v-if="store.status?.thingElevated" variant="outline" class="border-emerald-500/50 text-emerald-600 dark:text-emerald-400">
|
||||
<ShieldCheck class="size-2.5 mr-0.5" />永久提权
|
||||
</Badge>
|
||||
<Badge v-else-if="store.status?.elevated" variant="outline" class="border-violet-500/50 text-violet-600 dark:text-violet-400">
|
||||
<KeyRound class="size-2.5 mr-0.5" />提权模式
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<p class="text-xs leading-relaxed">
|
||||
LibreHardwareMonitor 访问 SMBus、部分 EC 传感器、某些 GPU 传感器需要管理员权限。
|
||||
默认非提权运行,覆盖大部分 CPU/GPU 温度(通过 OHM RPC/WMI 仍可读),牺牲部分主板/电压传感器。
|
||||
LibreHardwareMonitor 访问 CPU MSR(温度/时钟)、存储 SMART、SMBus、EC 传感器需要管理员权限。
|
||||
普通权限下可读:CPU 负载/功率、内存、GPU 负载。不可读:CPU 温度/时钟、存储、主板/电压。
|
||||
</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>
|
||||
<!-- 两种提权模式说明 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<KeyRound class="size-3.5 mt-0.5 shrink-0 text-orange-500" />
|
||||
<div class="text-xs">
|
||||
<span class="font-medium text-foreground">提权</span>:仅以管理员权限启动 ThingHK(弹 UAC)。ThingHK 崩溃不自动重启,停止走 /shutdown 接口。
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<ShieldCheck class="size-3.5 mt-0.5 shrink-0 text-emerald-500" />
|
||||
<div class="text-xs">
|
||||
<span class="font-medium text-foreground">永久提权</span>:以管理员权限重启 Thing 自身,ThingHK 子进程继承权限。标志持久化,后续每次启动自动触发 UAC;可在概览页点击"取消提权"关闭。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="store.status?.thingElevated" class="text-xs leading-relaxed text-emerald-600 dark:text-emerald-400 flex items-start gap-1.5">
|
||||
<ShieldCheck class="size-3.5 mt-0.5 shrink-0" />
|
||||
<span>永久提权模式:Thing 与 ThingHK 均以管理员权限运行,ProcessManager 可直接 kill,崩溃自动重启可用。</span>
|
||||
</p>
|
||||
<p v-else-if="store.status?.elevated" class="text-xs leading-relaxed text-violet-600 dark:text-violet-400 flex items-start gap-1.5">
|
||||
<KeyRound class="size-3.5 mt-0.5 shrink-0" />
|
||||
<span>提权模式运行中:仅 ThingHK 管理员权限,进程不归 ProcessManager 管控,崩溃不自动重启;停止通过 /shutdown 接口优雅退出。</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -590,4 +968,76 @@ watch(() => store.status?.ready, (ready, prev) => {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<!-- 硬件监控配置 Dialog -->
|
||||
<Dialog v-model:open="configDialogOpen">
|
||||
<DialogContent class="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<ListChecks class="size-4 text-primary" />监控项配置
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
勾选需要监控的硬件和传感器类型。硬件开关变更需重启 Kernel,传感器类型过滤立即生效。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea class="max-h-[60vh] pr-4">
|
||||
<div class="space-y-4">
|
||||
<!-- 硬件分组 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-medium text-muted-foreground">硬件分组</h4>
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
v-for="hw in store.hardwareConfig?.availableHardware ?? []"
|
||||
:key="hw.key"
|
||||
class="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Checkbox
|
||||
:id="'hw-' + hw.key"
|
||||
:model-value="editingHardware[hw.key] ?? false"
|
||||
@update:model-value="editingHardware[hw.key] = Boolean($event)"
|
||||
/>
|
||||
<label :for="'hw-' + hw.key" class="text-sm cursor-pointer truncate">{{ hw.name }}</label>
|
||||
<span v-if="hw.key === 'motherboard'" class="text-[10px] text-muted-foreground/60">含 SuperIO/EC(自动联动)</span>
|
||||
</div>
|
||||
<Badge v-if="hw.requiresAdmin" variant="outline" class="text-[10px] shrink-0 text-amber-600 border-amber-500/40">
|
||||
需管理员
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- 传感器类型 -->
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-medium text-muted-foreground">传感器类型</h4>
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<div
|
||||
v-for="st in store.hardwareConfig?.availableSensorTypes ?? []"
|
||||
:key="st.key"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
||||
>
|
||||
<Checkbox
|
||||
:id="'st-' + st.key"
|
||||
:model-value="editingSensorTypes[st.key] ?? false"
|
||||
@update:model-value="editingSensorTypes[st.key] = Boolean($event)"
|
||||
/>
|
||||
<label :for="'st-' + st.key" class="text-sm cursor-pointer">{{ st.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="configDialogOpen = false">取消</Button>
|
||||
<Button :disabled="savingConfig" @click="handleSaveConfig">
|
||||
<Loader2 v-if="savingConfig" class="size-3.5 animate-spin" />
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user