托盘调整

This commit is contained in:
2026-07-30 18:54:45 +08:00
parent f452322aad
commit 9d8f963cc6
14 changed files with 1441 additions and 659 deletions
+199 -263
View File
@@ -44,20 +44,6 @@ const tabsListRef = useModuleTabs(activeTab, [
{ value: 'settings', label: '设置' },
])
// ===== 关键指标历史 buffer(用于概览页 sparkline,不引入外部图表库) =====
// 容量 30 点,约对应 30 秒(fast 通道 1s 推送一次)
const HISTORY_LEN = 30
const cpuTempHistory = 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
buf.value.push(v)
if (buf.value.length > HISTORY_LEN) buf.value.shift()
}
// ===== 关键指标 computed =====
/** GPU 分组 id(兼容 intel/amd/nvidia */
@@ -69,9 +55,34 @@ const gpuGroupId = computed(() => {
})
const cpuModel = computed(() => store.groupById['cpu']?.sensors[0]?.hardwareName ?? null)
const cpuTemp = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'temperature' }))
/** 取首个 >0 的传感器值:用于过滤 AMD Ryzen 上 LHB 返回 0 的无效读数(温度/功耗/时钟为 0 不可信) */
function findPositiveValue(groupId: string, matchers: Array<{ name?: string; hardwareName?: string; type?: string }>): number | null {
for (const m of matchers) {
const v = store.findSensorValue(groupId, m)
if (v != null && v > 0 && isFinite(v)) return v
}
return null
}
// CPU 温度:Intel 命名 "CPU Package"AMD Ryzen 命名 "Core (Tctl/Tdie)"
// LHB 0.9.5 对 Ryzen 9000 系列支持不全,可能返回 0,用 >0 守卫过滤。
const cpuTemp = computed(() =>
findPositiveValue('cpu', [
{ name: 'CPU Package', type: 'temperature' }, // Intel
{ name: 'Core (Tctl/Tdie)', type: 'temperature' }, // AMD Ryzen
{ type: 'temperature' }, // 兜底:任意温度
])
)
const cpuLoad = computed(() => store.findSensorValue('cpu', { name: 'CPU Total', type: 'load' }))
const cpuPower = computed(() => store.findSensorValue('cpu', { name: 'CPU Package', type: 'power' }))
// CPU 功耗:Intel "CPU Package"AMD Ryzen "Package"
const cpuPower = computed(() =>
findPositiveValue('cpu', [
{ name: 'CPU Package', type: 'power' }, // Intel
{ name: 'Package', type: 'power' }, // AMD Ryzen
{ type: 'power' }, // 兜底:任意功耗
])
)
const gpuModel = computed(() => {
const gid = gpuGroupId.value
@@ -121,7 +132,10 @@ const memModuleModels = computed(() => {
return Array.from(models)
})
/** 存储硬盘列表(按 hardwareName 分组,提取温度/使用率/容量) */
/** 存储硬盘列表(按 hardwareName 分组,提取温度/使用率/容量)
* LHB 0.9.5 已知 bugMBR + XINT13 Extended 分区布局下,Free Space 可能返回
* ≈ 2^64/1e9 GB(无符号回绕),Used Space load 也可能为巨大负值。
* 这里对 LHB 原始值做有效性校验,异常时降级为 null(UI 显示 "--")。 */
interface StorageDrive {
name: string
temp: number | null
@@ -139,28 +153,34 @@ const storageDrives = computed<StorageDrive[]>(() => {
}
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
// Used Space 是 load 百分比,理论上 [0,100]LHB 在 MBR+Extended 布局下可能返回 -922153000% 等
const rawUsedPct = sensors.find(s => s.name === 'Used Space' && s.type === 'load')?.value ?? null
const usedPct = rawUsedPct != null && isFinite(rawUsedPct) && rawUsedPct >= 0 && rawUsedPct <= 100
? rawUsedPct
: null
// Total Space 在 LHB 中通常正确(即便 Free Space 异常)
const rawTotal = sensors.find(s => s.name === 'Total Space' && s.type === 'data')?.value ?? null
const totalGB = rawTotal != null && isFinite(rawTotal) && rawTotal > 0 && rawTotal < 1e6
? rawTotal
: null
// Free Space 在 MBR+Extended 布局下可能返回 ≈ 1.8446744e10 GBUINT64_MAX / 1e9
const rawFree = sensors.find(s => s.name === 'Free Space' && s.type === 'data')?.value ?? null
const freeGB = rawFree != null && isFinite(rawFree) && rawFree >= 0 && totalGB != null && rawFree <= totalGB
? rawFree
: null
// 优先用 total - free 计算 usedGBfree 异常时回退用 usedPct * total / 100
let usedGB: number | null = null
if (totalGB != null) {
if (freeGB != null) {
usedGB = totalGB - freeGB
} else if (usedPct != null) {
usedGB = totalGB * usedPct / 100
}
}
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))
@@ -258,22 +278,6 @@ const stateMeta: Record<ConnectionState, { text: string; class: string }> = {
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 ?? [])
@@ -651,15 +655,20 @@ const osdConfig = ref<OsdConfig>(loadOsdConfig())
/** 传感器名称中英文字典(覆盖常见 LHB 传感器名 + 硬件名) */
const SENSOR_NAME_ZH: Record<string, string> = {
// CPU
'CPU Package': 'CPU 封装',
'CPU Total': 'CPU 总负载',
'CPU Core Average': 'CPU 平均',
'CPU Graphics': 'CPU 核显',
'CPU DRAM': 'CPU 内存',
'CPU Cores': 'CPU 核心',
'CPU Bus': 'CPU 总线',
'CPU Core': 'CPU 核心',
// CPU 通用(Intel 命名)
'CPU Total': '总使用率',
'CPU Core Average': '平均温度',
'CPU Core Max': '最大使用率',
'CPU Graphics': '核显',
'CPU DRAM': '内存功耗',
'CPU Cores': '核心功耗',
'CPU Bus': '总线功耗',
'Bus Speed': '总线频率',
'Distance to TjMax': 'TjMax 距离',
// AMD Ryzen
'Core (Tctl/Tdie)': '封装温度',
'Cores (Average)': '平均频率',
'Cores (Average Effective)': '平均有效频率',
// GPU
'GPU Core': 'GPU 核心',
'GPU Memory': 'GPU 显存',
@@ -672,11 +681,13 @@ const SENSOR_NAME_ZH: Record<string, string> = {
'GPU Memory Total': '总显存',
'GPU Memory Used': '已用显存',
'D3D 3D': '3D 引擎',
'D3D Copy': '拷贝引擎',
'D3D VideoDecode': '视频解码',
'D3D VideoProcessing': '视频处理',
// 内存
'Memory': '内存',
'Memory Used': '已用',
'Memory Available': '可用',
'Virtual Memory': '虚拟内存',
'Virtual Memory': '虚拟内存使用率',
'Virtual Memory Used': '虚拟已用',
'Virtual Memory Available': '虚拟可用',
// 存储
@@ -692,7 +703,7 @@ const SENSOR_NAME_ZH: Record<string, string> = {
'System Fan': '系统风扇',
'Motherboard': '主板',
'Motherboard Temperature': '主板温度',
'CPU Socket': 'CPU 插槽',
'CPU Socket': 'CPU 插槽温度',
// 电池
'Battery Level': '电量',
'Battery Charge': '充电功率',
@@ -702,6 +713,23 @@ const SENSOR_NAME_ZH: Record<string, string> = {
'Battery Wear Level': '电池损耗',
}
/** 同名传感器按 type 消歧的字典(key 格式:sensorName|type)。
* 用于解决 Intel/AMD 同一传感器名在不同 type 下含义不同的问题。
* 例如 'CPU Package' 在 temperature 是封装温度,在 power 是封装功耗;
* 'Memory' 在 load 是使用率,在 data 是内存容量。 */
const SENSOR_NAME_ZH_TYPED: Record<string, string> = {
'CPU Package|temperature': '封装温度',
'CPU Package|power': '封装功耗',
'Package|power': '封装功耗',
'Package|temperature': '封装温度',
'Memory|load': '内存使用率',
'Memory|data': '内存',
'Memory|smalldata': '内存',
'Cores (Average)|clock': '平均频率',
'Cores (Average)|power': '核心平均功耗',
'Cores (Average Effective)|clock': '平均有效频率',
}
/** 硬件名中英文(用于分组标题等) */
const HW_NAME_ZH: Record<string, string> = {
'Total Memory': '总内存',
@@ -775,7 +803,8 @@ function fullColloquialLabel(item: OsdItem): string {
return hw || type || (SENSOR_NAME_ZH[item.sensorName] ?? item.sensorName)
}
/** 传感器显示名翻译:根据 labelLanguage 返回中文或英文 */
/** 传感器显示名翻译:根据 labelLanguage 返回中文或英文
* 中文优先智能"按词对应"翻译,找不到时回退到通俗组合 */
function sensorLabel(item: OsdItem): string {
// 特殊项
if (item.special) {
@@ -784,7 +813,10 @@ function sensorLabel(item: OsdItem): string {
return osdConfig.value.labelLanguage === 'zh' ? meta.zh : meta.en
}
if (osdConfig.value.labelLanguage === 'en') return item.sensorName
// 使用通俗描述:CPU温度、CPU功耗、CPU使用率 等
// 优先:智能精准翻译(与 Dialog 保持一致)
const smart = smartSensorLabelZh(item.sensorName, item.type)
if (smart) return smart
// 回退:通俗描述
return fullColloquialLabel(item)
}
@@ -795,8 +827,7 @@ function hwLabel(item: OsdItem): string {
}
/** AvailableSensor 的传感器名翻译(用于选择 Dialog)
* 使用更明显的名字:负载→使用率,温度/功率→XXX温度/XXX功率
* 容量类(data/smalldata)用字典翻译区分已用/可用,避免重名 */
* 优先智能"按词对应"翻译(精准且唯一),找不到时回退到通俗组合。 */
function sensorLabelAvail(s: AvailableSensor): string {
if (s.special) {
const meta = SPECIAL_SENSOR_META[s.special]
@@ -804,26 +835,28 @@ function sensorLabelAvail(s: AvailableSensor): string {
return osdConfig.value.labelLanguage === 'zh' ? meta.zh : meta.en
}
if (osdConfig.value.labelLanguage === 'en') return s.sensorName
// 优先:智能精准翻译(typed 字典 + 字典 + 模式匹配)
const smart = smartSensorLabelZh(s.sensorName, s.type)
if (smart) return smart
// 容量类(data/smalldata):用字典翻译 + 硬件名区分(已用内存/可用内存/已用空间/可用空间)
if (s.type === 'data' || s.type === 'smalldata') {
const dictName = SENSOR_NAME_ZH[s.sensorName]
if (dictName) {
const hw = shortHardwareLabelAvail(s)
// 存储类用硬盘型号
const prefix = s.groupId === 'storage' ? truncateHwName(s.hardwareName) : hw
return prefix ? `${prefix}${dictName}` : dictName
}
return SENSOR_NAME_ZH[s.sensorName] ?? s.sensorName
}
// 中文:用 通俗类型后缀 生成更明显的名字(如 CPU温度、CPU使用率、GPU功耗
// 回退:通俗类型组合(短前缀 + 类型后缀
const type = colloquialTypeLabel({ type: s.type } as OsdItem)
const hw = shortHardwareLabelAvail(s)
// 存储类用硬件名(硬盘型号)+ 类型
if (s.groupId === 'storage') {
return `${truncateHwName(s.hardwareName)}${type}`
}
if (hw && type) return `${hw}${type}`
// 回退到字典翻译
return SENSOR_NAME_ZH[s.sensorName] ?? s.sensorName
}
@@ -851,6 +884,76 @@ function hwLabelAvail(s: AvailableSensor): string {
return HW_NAME_ZH[s.hardwareName] ?? s.hardwareName
}
/** 类型后缀(中文,用于组合"核心 N 频率"、"封装温度"等精准翻译) */
function typeSuffixZh(type: string): string {
switch (type) {
case 'temperature': return '温度'
case 'load': return '使用率'
case 'power': return '功耗'
case 'clock': return '频率'
case 'voltage': return '电压'
case 'fan': return '风扇'
case 'throughput': return '速率'
case 'level': return '等级'
case 'frequency': return '频率'
case 'control': return '控制'
case 'factor': return '因子'
case 'timespan': return '时长'
case 'energy': return '能量'
default: return ''
}
}
/** 智能"按词对应"翻译:根据 sensorName + type 生成精准中文标签
* 优先级:① typed 字典(同名不同义,如 CPU Package temperature vs power
* ② 普通字典(固定短语)
* ③ 模式匹配(带核心序号、CCD、D3D 引擎等)
* ④ 返回 null 交由调用方回退
* 设计目标:每个传感器产出唯一中文标签,避免"CPU功耗 ×8"这种重复 */
function smartSensorLabelZh(name: string, type: string): string | null {
// ① typed 字典:同名 + type 消歧
const typedKey = `${name}|${type}`
if (SENSOR_NAME_ZH_TYPED[typedKey]) return SENSOR_NAME_ZH_TYPED[typedKey]
// ② 普通字典
if (SENSOR_NAME_ZH[name]) return SENSOR_NAME_ZH[name]
// ③ 模式匹配
let m: RegExpMatchArray | null
// CPU Core #N / Core #N → 核心 N + 类型后缀(load→使用率, clock→频率, power→功耗...
if ((m = name.match(/^(?:CPU )?Core #(\d+)$/))) {
return `核心 ${m[1]}${typeSuffixZh(type)}`
}
// CPU Core #N (Effective) / Core #N (Effective) → 核心 N 有效频率
if ((m = name.match(/^(?:CPU )?Core #(\d+) \(Effective\)$/))) {
return `核心 ${m[1]} 有效${typeSuffixZh(type)}`
}
// Core #N (SMU) → 核心 N (SMU) 功耗
if ((m = name.match(/^Core #(\d+) \(SMU\)$/))) {
return `核心 ${m[1]} (SMU)${typeSuffixZh(type)}`
}
// CCD1 (Tdie) → CCD1 (Tdie) 温度
if ((m = name.match(/^CCD(\d+) \(Tdie\)$/))) {
return `CCD${m[1]} (Tdie) 温度`
}
// CCD#N (Tccd#N) → CCD#N (Tccd#N) 温度
if ((m = name.match(/^CCD(\d+) \(Tccd\d+\)$/))) {
return `CCD${m[1]} 温度`
}
// D3D 引擎族:D3D 3D / D3D Copy / D3D VideoDecode / D3D VideoProcessing
if ((m = name.match(/^D3D\s+(.+)$/))) {
const d3dMap: Record<string, string> = {
'3D': '3D 引擎',
'Copy': '拷贝引擎',
'VideoDecode': '视频解码',
'VideoProcessing': '视频处理',
}
if (d3dMap[m[1]]) return d3dMap[m[1]]
}
return null
}
/**
* 判断传感器是否为"常用项"。
* 规则:基于 name + type + hardwareName 模式匹配,挑选日常监控最关注的指标。
@@ -864,14 +967,14 @@ function isCommon(s: { sensorName: string; hardwareName: string; type: string; s
const hw = s.hardwareName.toLowerCase()
const nameLc = name.toLowerCase()
// CPU 常用:Package 温度 / Total 负载 / Package 功耗 / Graphics 核显 / Core Average 温度
// CPU 常用:封装温度 / Total 负载 / 封装功耗 / Graphics 核显 / Core Average 温度
// 兼容 Intel ("CPU Package") 与 AMD Ryzen ("Core (Tctl/Tdie)" / "Package") 命名
// 其余(分核负载、分核温度、时钟、总线、各路功耗)归详细
if (hw.includes('cpu') || nameLc.startsWith('cpu')) {
if (name === 'CPU Package' && type === 'temperature') return true
if (hw.includes('cpu') || nameLc.startsWith('cpu') || hw.includes('amd ryzen')) {
if (type === 'temperature' && (name === 'CPU Package' || name === 'Core (Tctl/Tdie)' || name === 'CPU Core Average')) return true
if (name === 'CPU Total' && type === 'load') return true
if (name === 'CPU Package' && type === 'power') return true
if (type === 'power' && (name === 'CPU Package' || name === 'Package')) return true
if (name === 'CPU Graphics' && (type === 'load' || type === 'temperature')) return true
if (name === 'CPU Core Average' && type === 'temperature') return true
return false
}
@@ -951,6 +1054,8 @@ const availableSensors = computed<AvailableSensor[]>(() => {
special: 'net-down',
})
for (const g of store.snapshot?.groups ?? []) {
// 悬浮窗不显示存储分组(硬盘容量/温度等已在主界面监控,OSD 场景无需)
if (g.id === 'storage') continue
const groupName = groupMeta[g.id]?.name ?? g.name
for (const s of g.sensors) {
const key = `${g.id}/${s.hardwareName}/${s.name}/${s.type}`.replace(/\s+/g, '_').toLowerCase()
@@ -1019,7 +1124,11 @@ const osdPickSelected = ref<Record<string, boolean>>({})
function openOsdPickDialog() {
const items = osdConfig.value.overlayItems
const selected: Record<string, boolean> = {}
for (const it of items) selected[it.key] = true
// 存储分组已从 OSD 选择中移除,已选的存储项不预选(确认时自动清理)
for (const it of items) {
if (it.groupId === 'storage') continue
selected[it.key] = true
}
osdPickSelected.value = selected
osdPickDialogOpen.value = true
}
@@ -1028,8 +1137,9 @@ function confirmOsdPick() {
// 收集所有勾选项(保留原有项对象,新增项从 availableSensors 构造)
const oldItems = osdConfig.value.overlayItems
const newItems: OsdItem[] = []
// 保留原有项
// 保留原有项(存储项不再支持,直接丢弃)
for (const old of oldItems) {
if (old.groupId === 'storage') continue
if (osdPickSelected.value[old.key]) {
newItems.push(old)
}
@@ -1048,13 +1158,12 @@ function confirmOsdPick() {
})
}
}
// 按默认顺序排序:CPU → GPU → 内存 → 网络 → 存储 → 其余
// 按默认顺序排序:CPU → GPU → 内存 → 网络 → 其余
const groupOrder = (id: string): number => {
if (id === 'cpu') return 0
if (id.startsWith('gpu')) return 1
if (id === 'memory') return 2
if (id === 'network') return 3
if (id === 'storage') return 4
return 9
}
newItems.sort((a, b) => groupOrder(a.groupId) - groupOrder(b.groupId))
@@ -1138,53 +1247,6 @@ function onBgAlphaInput(alpha: number) {
updateOsdConfig('bgColor', `rgba(${r}, ${g}, ${b}, ${a})`)
}
/** 根据 OSD item 查找当前快照中的传感器值(支持特殊项) */
function getOsdItemValue(item: OsdItem): number | null {
// 特殊项:网速
if (item.special === 'net-up') return store.networkSpeed?.uploadBps ?? null
if (item.special === 'net-down') return store.networkSpeed?.downloadBps ?? null
const g = store.groupById[item.groupId]
if (!g) return null
const s = g.sensors.find(s =>
s.hardwareName === item.hardwareName && s.name === item.sensorName
)
return s?.value ?? null
}
/** 格式化 OSD 显示值(简洁模式,不含单位后缀;网速已含单位字符串) */
function fmtOsdValue(v: number | null, type: string, _unit: string, special?: string): string {
if (v == null || !isFinite(v)) return '--'
// 网速特殊项:自适应 KB/s 或 MB/s(值已含单位字符串)
if (special === 'net-up' || special === 'net-down') {
if (v >= 1_048_576) return (v / 1_048_576).toFixed(2) + ' MB/s'
if (v >= 1024) return (v / 1024).toFixed(1) + ' KB/s'
return v.toFixed(0) + ' B/s'
}
const digits = (type === 'voltage' || type === 'power') ? 2
: (type === 'temperature' || type === 'load' || type === 'level') ? 0
: 1
return v.toFixed(digits)
}
/** 获取单位后缀(简洁模式,网速已含单位返回空) */
function unitSuffix(item: OsdItem): string {
if (item.special === 'net-up' || item.special === 'net-down') return ''
if (!osdConfig.value.showUnit) return ''
switch (item.type) {
case 'temperature': return '°C'
case 'load': return '%'
case 'power': return 'W'
case 'voltage': return 'V'
case 'fan': return 'RPM'
case 'clock':
case 'frequency': return 'MHz'
case 'data':
case 'smalldata': return 'GB'
case 'level': return '%'
default: return item.unit || ''
}
}
/** 将 hex 颜色 + 不透明度(0-100) 转为 rgba 字符串 */
function withOpacity(hex: string, opacityPct: number): string {
const a = Math.max(0, Math.min(100, opacityPct)) / 100
@@ -1210,42 +1272,6 @@ function osdItemColor(item: OsdItem): string {
return withOpacity(osdConfig.value.fontColor, opacity)
}
/** 预览用分组(group / multiline 布局) */
const osdPreviewGroups = computed(() => {
const items = osdConfig.value.overlayItems
if (!items?.length) return []
const groups: { key: string; label: string; items: OsdItem[] }[] = []
const map = new Map<string, { key: string; label: string; items: OsdItem[] }>()
const isEn = osdConfig.value.labelLanguage === 'en'
for (const item of items) {
let gkey: string
if (item.special === 'net-up' || item.special === 'net-down') gkey = 'network'
else if (item.groupId.startsWith('gpu')) gkey = 'gpu'
else gkey = item.groupId
let g = map.get(gkey)
if (!g) {
const label = isEn
? ({ cpu: 'CPU', gpu: 'GPU', memory: 'RAM', storage: 'DISK', network: 'NET' }[gkey] ?? gkey.toUpperCase().slice(0, 6))
: ({ cpu: 'CPU', gpu: 'GPU', memory: '内存', storage: '存储', network: '网络' }[gkey] ?? gkey)
g = { key: gkey, label, items: [] }
map.set(gkey, g)
groups.push(g)
}
g.items.push(item)
}
return groups
})
/** 预览用布局标签 */
const osdLayoutLabel = computed(() => {
switch (osdConfig.value.layout) {
case 'single': return '单行'
case 'group': return '分组横排'
case 'multiline': return '多行'
default: return ''
}
})
// ===== OSD 窗口管理(实际创建/隐藏 Tauri 窗口并推送数据) =====
const OSD_OVERLAY_LABEL = 'osd-overlay'
/** 抑制百分比 watch 的程序定位标志:拖动 onMoved 更新百分比时置 true,避免触发 resetOverlayPosition 循环 */
@@ -1732,7 +1758,7 @@ watch([
<!-- 始终显示卡片网格未启动时数据以占位符显示保持画面完整 -->
<div v-else key="content" class="grid grid-cols-1 md:grid-cols-3 gap-2.5">
<!-- CPU温度 + 功耗 + 曲线图未读数据以 -- 占位 -->
<!-- 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">
@@ -1741,7 +1767,7 @@ watch([
</CardTitle>
</CardHeader>
<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>
@@ -1753,9 +1779,6 @@ watch([
<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>
@@ -1768,7 +1791,7 @@ watch([
</CardContent>
</Card>
<!-- GPU结构与 CPU 一致温度 + 功耗 + 曲线图未读数据以 -- 占位 -->
<!-- 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">
@@ -1777,7 +1800,7 @@ watch([
</CardTitle>
</CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
<!-- 温度 + 功耗 + 曲线图 CPU 卡片结构一致 -->
<!-- 温度 + 功耗 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>
@@ -1789,9 +1812,6 @@ watch([
<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>
<!-- 负载 -->
<div>
@@ -1835,7 +1855,7 @@ watch([
</CardContent>
</Card>
<!-- 网络跨3列下载/上传 + 曲线图独立于 Kernel Tauri 后台推送 -->
<!-- 网络跨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">
@@ -1847,25 +1867,15 @@ watch([
<!-- 下载 -->
<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 class="text-2xl font-bold tabular-nums leading-tight text-sky-500">
{{ downSpeed.value }}<span class="text-sm font-normal text-muted-foreground ml-0.5">{{ downSpeed.unit }}</span>
</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 class="text-2xl font-bold tabular-nums leading-tight text-violet-500">
{{ upSpeed.value }}<span class="text-sm font-normal text-muted-foreground ml-0.5">{{ upSpeed.unit }}</span>
</div>
</div>
</div>
@@ -2136,9 +2146,6 @@ watch([
<span class="truncate">{{ sensorLabel(item) }}</span>
<span class="text-xs text-muted-foreground truncate">/ {{ hwLabel(item) }}</span>
</div>
<span class="font-mono text-xs tabular-nums shrink-0 text-muted-foreground">
{{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }}
</span>
<Button size="icon-xs" variant="ghost" class="text-muted-foreground hover:text-destructive shrink-0" @click="removeOsdItem(item.key)">
<span class="text-lg leading-none">×</span>
</Button>
@@ -2651,78 +2658,7 @@ watch([
</CardContent>
</Card>
<!-- 预览 -->
<Card v-if="osdConfig.overlayEnabled && osdConfig.overlayItems.length">
<CardHeader>
<CardTitle class="text-base flex items-center gap-2">
<Eye class="size-4 text-primary" />预览
</CardTitle>
</CardHeader>
<CardContent>
<div class="text-xs text-muted-foreground mb-1.5">悬浮窗{{ osdLayoutLabel }}</div>
<div class="rounded-md bg-muted/30 border p-3 font-mono">
<!-- 单行预览分组式组间用 | 分隔 -->
<div
v-if="osdConfig.layout === 'single'"
class="osd-preview-single"
:style="{ background: osdConfig.bgColor, fontSize: osdConfig.fontSize + 'px' }"
>
<template v-for="(g, gi) in osdPreviewGroups" :key="g.key">
<span v-if="gi > 0" class="osd-preview-sg-sep">|</span>
<span class="osd-preview-sg-group">
<span v-if="osdConfig.showLabel" class="osd-preview-sg-label">{{ g.label }}</span>
<span
v-for="item in g.items"
:key="item.key"
:style="{ color: osdItemColor(item) }"
>
{{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }}
</span>
</span>
</template>
</div>
<!-- 分组横排预览 -->
<div
v-else-if="osdConfig.layout === 'group'"
class="osd-preview-group"
:style="{ background: osdConfig.bgColor, fontSize: osdConfig.fontSize + 'px' }"
>
<div v-for="g in osdPreviewGroups" :key="g.key" class="osd-preview-group-item">
<div class="osd-preview-group-header">{{ g.label }}</div>
<div class="osd-preview-group-data">
<span
v-for="item in g.items"
:key="item.key"
class="osd-preview-group-val"
:style="{ color: osdItemColor(item) }"
>
{{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }}
</span>
</div>
</div>
</div>
<!-- 多行预览 -->
<div
v-else-if="osdConfig.layout === 'multiline'"
class="osd-preview-multiline"
:style="{ background: osdConfig.bgColor, fontSize: osdConfig.fontSize + 'px' }"
>
<div v-for="g in osdPreviewGroups" :key="g.key" class="osd-preview-line">
<span class="osd-preview-line-label">{{ g.label }}</span>
<span
v-for="item in g.items"
:key="item.key"
:style="{ color: osdItemColor(item) }"
>
{{ fmtOsdValue(getOsdItemValue(item), item.type, item.unit, item.special) }}{{ unitSuffix(item) }}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- 预览卡片已移除用户可在桌面悬浮窗直接看实际效果 -->
</div>
</ScrollArea>
</TabsContent>