托盘调整
This commit is contained in:
@@ -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 已知 bug:MBR + 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 GB(UINT64_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 计算 usedGB;free 异常时回退用 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>
|
||||
|
||||
Reference in New Issue
Block a user