性能优化

This commit is contained in:
zhongluofeng
2026-08-06 10:33:16 +08:00
parent c7578a2e6b
commit e66c53e66d
105 changed files with 7273 additions and 5002 deletions
+41 -659
View File
@@ -12,11 +12,19 @@ import { toast } from 'vue-sonner'
import { VueDraggable } from 'vue-draggable-plus'
import { appDataDir } from '@tauri-apps/api/path'
import { revealItemInDir } from '@tauri-apps/plugin-opener'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
import { currentMonitor, LogicalPosition, LogicalSize } from '@tauri-apps/api/window'
import { useMonitorStore, type SensorEntry, type SensorGroup, type ConnectionState } from '@/stores/monitorStore'
import { useModuleTabs } from '@/lib/useModuleTabs'
import {
useMonitorStore,
type SensorEntry,
type SensorGroup,
type ConnectionState,
type OsdConfig,
type OsdItem,
type ColorTheme,
type AlertConfig,
DEFAULT_COLOR_THEME,
} from '@/stores/monitorStore'
import { useModuleTabs } from '@/lib/use-module-tabs'
import { fmt, tempColor, loadColor, fmtSpeed, typeLabel, groupDisplayName, groupIcon } from './format'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -185,90 +193,6 @@ const storageDrives = computed<StorageDrive[]>(() => {
const downSpeed = computed(() => fmtSpeed(store.networkSpeed?.downloadBps ?? null))
const upSpeed = computed(() => fmtSpeed(store.networkSpeed?.uploadBps ?? null))
// ===== 工具函数 =====
/** 格式化数值:整数型指标(负载/温度)保留 0 位,浮点型(电压/功率)保留 2 位 */
function fmt(v: number | null, digits = 1): string {
if (v == null || !isFinite(v)) return '--'
return v.toFixed(digits)
}
/** 温度颜色:绿(<50) → 黄(<70) → 橙(<85) → 红(>=85) */
function tempColor(t: number | null): string {
if (t == null) return 'text-muted-foreground'
if (t < 50) return 'text-emerald-500'
if (t < 70) return 'text-yellow-500'
if (t < 85) return 'text-orange-500'
return 'text-red-500'
}
/** 负载颜色:蓝(<50) → 紫(<80) → 红(>=80) */
function loadColor(v: number | null): string {
if (v == null) return 'text-muted-foreground'
if (v < 50) return 'text-sky-500'
if (v < 80) return 'text-violet-500'
return 'text-red-500'
}
/** 格式化网速(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: '温度',
load: '负载',
power: '功率',
voltage: '电压',
fan: '风扇',
clock: '时钟',
data: '容量',
smalldata: '容量',
throughput: '吞吐',
level: '等级',
control: '控制',
frequency: '频率',
factor: '因子',
timespan: '时长',
energy: '能量',
noise: '噪声',
conductivity: '电导率',
humidity: '湿度',
flow: '流量',
}
function typeLabel(t: string): string {
return typeLabels[t] ?? t
}
/** 分组 id → 显示名 + 图标组件 */
const groupMeta: Record<string, { name: string; icon: typeof Cpu }> = {
cpu: { name: 'CPU', icon: Cpu },
memory: { name: '内存', icon: MemoryStick },
gpuintel: { name: 'GPU', icon: Gauge },
gpuamd: { name: 'GPU', icon: Gauge },
gpunvidia: { name: 'GPU', icon: Gauge },
storage: { name: '存储', icon: HardDrive },
motherboard: { name: '主板', icon: Activity },
superio: { name: '超级 IO', icon: Activity },
embeddedcontroller: { name: '嵌入式控制器', icon: Activity },
battery: { name: '电池', icon: Activity },
network: { name: '网络', icon: Activity },
psu: { name: '电源', icon: Zap },
}
function groupDisplayName(id: string, fallback: string): string {
return groupMeta[id]?.name ?? fallback
}
function groupIcon(id: string): typeof Cpu {
return groupMeta[id]?.icon ?? Activity
}
// ===== 连接状态徽章 =====
const stateMeta: Record<ConnectionState, { text: string; class: string }> = {
idle: { text: '未启动', class: 'bg-muted text-muted-foreground' },
@@ -281,14 +205,19 @@ const stateMeta: Record<ConnectionState, { text: string; class: string }> = {
// ===== 分组列表(详细页用) =====
const groups = computed<SensorGroup[]>(() => store.snapshot?.groups ?? [])
/** 按 hardwareName 子分组,再按 type 二级分组(详细页用) */
/** 按 hardwareName 子分组,再按 type 二级分组(详细页用)
* 分组结果只依赖传感器的静态元数据(硬件名/类型),与数值变化无关;
* 以传感器数组引用为键缓存(WeakMap),避免每次渲染对数百传感器全量重算 */
const sensorGroupCache = new WeakMap<SensorEntry[], { hardware: string; byType: { type: string; items: SensorEntry[] }[] }[]>()
function groupSensors(sensors: SensorEntry[]): { hardware: string; byType: { type: string; items: SensorEntry[] }[] }[] {
const cached = sensorGroupCache.get(sensors)
if (cached) return cached
const byHw = new Map<string, SensorEntry[]>()
for (const s of sensors) {
if (!byHw.has(s.hardwareName)) byHw.set(s.hardwareName, [])
byHw.get(s.hardwareName)!.push(s)
}
return Array.from(byHw.entries()).map(([hw, items]) => {
const result = Array.from(byHw.entries()).map(([hw, items]) => {
const byType = new Map<string, SensorEntry[]>()
for (const s of items) {
if (!byType.has(s.type)) byType.set(s.type, [])
@@ -299,6 +228,8 @@ function groupSensors(sensors: SensorEntry[]): { hardware: string; byType: { typ
byType: Array.from(byType.entries()).map(([type, list]) => ({ type, items: list })),
}
})
sensorGroupCache.set(sensors, result)
return result
}
// ===== Accordion 折叠状态 =====
@@ -455,203 +386,12 @@ async function handleSaveConfig() {
}
}
// ===== OSD 显示配置 =====
// OSDOn-Screen Display)配置:控制传感器数据在桌面悬浮窗中的显示
// 配置持久化到 localStorage,由独立 OsdWindow.vue 消费。
/** OSD 显示项:从可用传感器中选取并排序 */
interface OsdItem {
/** 唯一 key{groupId}/{hardwareName}/{sensorName}/{type} 小写化,或 special 项的固定 key */
key: string
groupId: string
sensorName: string
hardwareName: string
type: string
unit: string
/** 特殊项标记:非 Kernel 传感器,由前端直接计算(如网速) */
special?: 'net-up' | 'net-down'
}
/** 颜色主题:按硬件/传感器类型着色(类似小飞机风格) */
interface ColorTheme {
/** 按 groupId 着色:cpu/gpu/memory/storage/... */
hardware: Record<string, string>
/** 按 sensor type 着色:temperature/load/power/... */
sensor: Record<string, string>
}
/** 警告色配置:阈值百分比 + 警告/严重颜色 */
interface AlertConfig {
/** 警告色开关 */
enabled: boolean
/** 警告阈值百分比(达到即变警告色,如 80) */
warnThreshold: number
/** 严重阈值百分比(达到即变严重色,如 90) */
criticalThreshold: number
/** 警告色(淡红,hex */
warnColor: string
/** 严重色(大红,hex */
criticalColor: string
/** 各硬件类型的最大值(用于将温度等非百分比值转为百分比)
* CPU 温度墙默认 100GPU 默认 85 */
maxValues: Record<string, number>
}
/** OSD 配置结构 */
interface OsdConfig {
overlayEnabled: boolean
overlayItems: OsdItem[]
/** 悬浮窗位置 X 百分比(0=最左,50=居中,100=最右) */
positionXPct: number
/** 悬浮窗位置 Y 百分比(0=最上,50=居中,100=最下) */
positionYPct: number
fontSize: number
showUnit: boolean
showLabel: boolean
/** 标题语言:'zh' 中文 / 'en' 英文(原始传感器名) */
labelLanguage: 'zh' | 'en'
/** 布局:'single' 单行分组式(组间用 | 分隔,固定宽度),
* 'group' 分组横排(标题在上+数据列在下),'multiline' 多行(每组一行,左对齐,类小飞机) */
layout: 'single' | 'group' | 'multiline'
updateIntervalMs: number
/** 鼠标穿透:true 时窗口不接收鼠标事件(需关闭穿透才能左键拖动) */
clickThrough: boolean
/** 默认文字颜色(hex),颜色主题关闭时使用 */
fontColor: string
/** 字体不透明度 0-100 */
fontOpacity: number
/** 悬浮窗背景色(CSS 颜色字符串,如 rgba(0,0,0,0.55) */
bgColor: string
/** 启用颜色主题(按硬件/传感器类型着色) */
colorThemeEnabled: boolean
/** 颜色主题配置 */
colorTheme: ColorTheme
/** 字体描边开关(默认关闭) */
fontStrokeEnabled: boolean
/** 字体描边厚度(px,默认 1) */
fontStrokeWidth: number
/** 字体描边颜色(hex,默认 #000000 */
fontStrokeColor: string
/** 警告色配置 */
alert: AlertConfig
/** 悬浮窗窗口保存位置(null=使用百分比计算默认位置) */
overlayX?: number | null
overlayY?: number | null
}
const OSD_STORAGE_KEY = 'thing_monitor_osd_config'
const OSD_CONFIG_VERSION = 11
/** 默认颜色主题(小飞机风格:不同硬件不同颜色,不同传感器不同颜色) */
const DEFAULT_COLOR_THEME: ColorTheme = {
hardware: {
cpu: '#4A9EFF',
gpuintel: '#9D4EFF',
gpuamd: '#9D4EFF',
gpunvidia: '#9D4EFF',
memory: '#FF9F4A',
storage: '#4AFF9F',
motherboard: '#FFD700',
superio: '#B0B0B0',
embeddedcontroller: '#B0B0B0',
battery: '#FF4A9F',
network: '#4AFFFF',
psu: '#FF4A4A',
},
sensor: {
temperature: '#FF6B6B',
load: '#4A9EFF',
power: '#FFD700',
voltage: '#9D4EFF',
fan: '#B0B0B0',
clock: '#4AFF9F',
data: '#FF9F4A',
smalldata: '#FF9F4A',
throughput: '#4AFFFF',
level: '#FF4A9F',
control: '#FFA500',
frequency: '#4AFF9F',
factor: '#FF4A4A',
timespan: '#B0B0B0',
energy: '#FFD700',
noise: '#B0B0B0',
conductivity: '#4AFFFF',
humidity: '#4A9EFF',
flow: '#4AFFFF',
},
}
/** 默认警告色配置:CPU 温度墙 100°C,GPU 85°C;百分比类直接用值 */
const DEFAULT_ALERT_CONFIG: AlertConfig = {
enabled: true,
warnThreshold: 80,
criticalThreshold: 90,
warnColor: '#FF6B6B',
criticalColor: '#FF0000',
maxValues: {
cpu: 100,
gpu: 85,
gpuintel: 85,
gpuamd: 85,
gpunvidia: 85,
},
}
function defaultOsdConfig(): OsdConfig {
return {
overlayEnabled: false,
overlayItems: [],
// 默认顶部居中(top 0):水平 50%,垂直 0%
positionXPct: 50,
positionYPct: 0,
fontSize: 14,
showUnit: true,
showLabel: true,
labelLanguage: 'zh',
layout: 'single',
updateIntervalMs: 1000,
// 默认关闭点击穿透:关闭后左键可直接拖动悬浮窗
clickThrough: false,
fontColor: '#ffffff',
fontOpacity: 100,
bgColor: 'transparent',
colorThemeEnabled: true,
colorTheme: { ...DEFAULT_COLOR_THEME },
fontStrokeEnabled: false,
fontStrokeWidth: 1,
fontStrokeColor: '#000000',
alert: { ...DEFAULT_ALERT_CONFIG, maxValues: { ...DEFAULT_ALERT_CONFIG.maxValues } },
overlayX: null,
overlayY: null,
}
}
function loadOsdConfig(): OsdConfig {
try {
const saved = localStorage.getItem(OSD_STORAGE_KEY)
if (!saved) return defaultOsdConfig()
const parsed = JSON.parse(saved)
if (parsed.version !== OSD_CONFIG_VERSION) return defaultOsdConfig()
// 合并默认值,确保新增字段有默认值
const def = defaultOsdConfig()
return { ...def, ...parsed.config }
} catch {
return defaultOsdConfig()
}
}
function saveOsdConfig(cfg: OsdConfig) {
try {
localStorage.setItem(OSD_STORAGE_KEY, JSON.stringify({
version: OSD_CONFIG_VERSION,
config: cfg,
}))
} catch {
/* 忽略 localStorage 写入失败 */
}
}
const osdConfig = ref<OsdConfig>(loadOsdConfig())
// ===== OSD 配置(由 monitorStore 统一管理,组件仅做 UI 展示与修改) =====
// 类型/默认值/持久化/窗口管理均在 monitorStoreApp 启动时由 store.initOsd() 显式初始化
const osdConfig = computed<OsdConfig>(() => store.osdConfig)
// 保存调用点保持简洁的薄包装(内部转发到 store 的持久化函数)
const saveOsdConfig = (cfg: OsdConfig) => store.saveOsdConfig(cfg)
const saveOsdConfigDebounced = (cfg: OsdConfig) => store.saveOsdConfigDebounced(cfg)
/** 传感器名称中英文字典(覆盖常见 LHB 传感器名 + 硬件名) */
const SENSOR_NAME_ZH: Record<string, string> = {
@@ -1056,7 +796,7 @@ const availableSensors = computed<AvailableSensor[]>(() => {
for (const g of store.snapshot?.groups ?? []) {
// 悬浮窗不显示存储分组(硬盘容量/温度等已在主界面监控,OSD 场景无需)
if (g.id === 'storage') continue
const groupName = groupMeta[g.id]?.name ?? g.name
const groupName = groupDisplayName(g.id, g.name)
for (const s of g.sensors) {
const key = `${g.id}/${s.hardwareName}/${s.name}/${s.type}`.replace(/\s+/g, '_').toLowerCase()
list.push({
@@ -1188,10 +928,10 @@ function removeOsdItem(key: string) {
}
}
/** OSD 配置项变更时自动保存 */
/** OSD 配置项变更时自动保存(防抖:滑块拖动期间不逐帧写盘) */
function updateOsdConfig(field: keyof OsdConfig, value: unknown) {
;(osdConfig.value as Record<string, unknown>)[field] = value
saveOsdConfig(osdConfig.value)
;(osdConfig.value as unknown as Record<string, unknown>)[field] = value
saveOsdConfigDebounced(osdConfig.value)
}
/** 解析背景色字符串为 hex + alpha0-100 */
@@ -1272,276 +1012,7 @@ function osdItemColor(item: OsdItem): string {
return withOpacity(osdConfig.value.fontColor, opacity)
}
// ===== OSD 窗口管理(实际创建/隐藏 Tauri 窗口并推送数据 =====
const OSD_OVERLAY_LABEL = 'osd-overlay'
/** 抑制百分比 watch 的程序定位标志:拖动 onMoved 更新百分比时置 true,避免触发 resetOverlayPosition 循环 */
let suppressPercentWatch = false
/** 构建用于 OSD 窗口的 URL(基于当前页面 URL 替换 hash) */
function osdUrl(hash: string): string {
const base = window.location.href.split('#')[0]
return `${base}#${hash}`
}
/** 推送当前 OSD 状态到所有 OSD 窗口 */
async function pushOsdState() {
const payload = {
config: osdConfig.value,
snapshot: store.snapshot,
networkSpeed: store.networkSpeed,
}
try {
await emit('osd-state-update', payload)
} catch (e) {
console.error('[OSD] 推送状态失败:', e)
}
}
/** 根据百分比位置计算窗口坐标 */
function computePositionFromPct(screenW: number, screenH: number, w: number, h: number, xPct: number, yPct: number): { x: number; y: number } {
// 百分比基于可用空间(屏幕尺寸 - 窗口尺寸),确保窗口不会被定位到屏幕外
const availW = Math.max(0, screenW - w)
const availH = Math.max(0, screenH - h)
return {
x: Math.round((availW * xPct) / 100),
y: Math.round((availH * yPct) / 100),
}
}
/** 根据显示项估算悬浮窗窗口尺寸(逻辑像素)
* single: 单行分组式,组间用 | 分隔,固定宽度数据列
* group: 分组横排,标题在上 + 数据列在下
* multiline: 多行,每组一行,标题 + 固定宽度数据列 */
function computeOsdWindowSize(
_itemCount: number,
layout: 'single' | 'group' | 'multiline',
fontSize: number,
_hasNetItem = false,
items?: OsdItem[],
): { w: number; h: number } {
const charW = fontSize * 0.62
const barHPad = 8 // osd-bar 左右 padding 4*2
// 按硬件类型分组(与渲染逻辑一致)
const groupMap = new Map<string, OsdItem[]>()
if (items?.length) {
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
if (!groupMap.has(gkey)) groupMap.set(gkey, [])
groupMap.get(gkey)!.push(item)
}
}
const groupCount = Math.max(1, groupMap.size)
// 每组数据列宽度:标签(6ch) + 各项(数值+单位+箭头/gap)
const groupWidths: number[] = []
for (const [, groupItems] of groupMap) {
const labelW = 6
const dataW = groupItems.reduce((sum, item) => {
const isNet = item.special === 'net-up' || item.special === 'net-down'
return sum + (isNet ? 11 : 8) + 1
}, 0)
groupWidths.push(labelW + dataW)
}
if (layout === 'multiline') {
// 多行:取最宽行
const maxLineW = groupWidths.length ? Math.max(...groupWidths) : 10
const w = Math.ceil(maxLineW * charW + barHPad)
const lineH = Math.ceil(fontSize + 2)
const h = Math.ceil(groupCount * lineH + 6)
return { w: Math.max(120, w), h: Math.max(28, h) }
}
if (layout === 'group') {
// 分组横排:各组横排 + 标题行
const totalW = groupWidths.reduce((s, w) => s + w + 4, 0) + (groupCount - 1) * 4
const w = Math.ceil(totalW * charW + barHPad)
const titleH = Math.ceil(fontSize * 0.85) + 2
const dataH = Math.ceil(fontSize) + 2
const h = Math.ceil(titleH + dataH + 10)
return { w: Math.max(120, w), h: Math.max(40, h) }
}
// single:单行分组式,各组横排 + 组间 | 分隔符(1ch)
const sepW = (groupCount - 1) * 1
const totalW = groupWidths.reduce((s, w) => s + w, 0) + sepW
const w = Math.ceil(totalW * charW + barHPad)
const h = Math.ceil(fontSize + 8)
return { w: Math.max(120, w), h: Math.max(28, h) }
}
/** 创建/显示悬浮窗窗口(默认置顶 + NoActivate + 点击穿透) */
async function ensureOverlayWindow() {
const existing = await WebviewWindow.getByLabel(OSD_OVERLAY_LABEL)
if (existing) {
// 窗口已存在,仅显示并推送最新状态
await existing.show()
await updateOsdWindowSize()
await pushOsdState()
return
}
// 获取屏幕尺寸用于定位
const monitor = await currentMonitor()
const screenW = monitor?.size.width ?? 1920
const screenH = monitor?.size.height ?? 1080
const scale = monitor?.scaleFactor ?? 1
const logicalW = screenW / scale
const logicalH = screenH / scale
const hasNetItem = osdConfig.value.overlayItems.some(i => i.special === 'net-up' || i.special === 'net-down')
const { w, h } = computeOsdWindowSize(
osdConfig.value.overlayItems.length,
osdConfig.value.layout,
osdConfig.value.fontSize,
hasNetItem,
osdConfig.value.overlayItems,
)
// 优先使用保存的像素位置;否则根据百分比计算默认位置
let x: number, y: number
if (osdConfig.value.overlayX != null && osdConfig.value.overlayY != null) {
x = osdConfig.value.overlayX
y = osdConfig.value.overlayY
} else {
const pos = computePositionFromPct(logicalW, logicalH, w, h, osdConfig.value.positionXPct, osdConfig.value.positionYPct)
x = pos.x
y = pos.y
}
const win = new WebviewWindow(OSD_OVERLAY_LABEL, {
url: osdUrl('osd-overlay'),
title: 'OSD 悬浮窗',
width: w,
height: h,
x,
y,
decorations: false,
transparent: true,
// 关闭窗口阴影:Win11 默认会画一圈阴影光晕,透明窗口上表现为可见的"外部框"
shadow: false,
alwaysOnTop: true,
skipTaskbar: true,
// 禁用调整大小:移除 Windows 隐形 resize 边框(该边框会拦截鼠标事件导致穿透/拖动失效)
resizable: false,
visible: true,
// 不获取焦点(NoActivate 由 Rust 后端 osd_apply_overlay_style 进一步保证)
focus: false,
})
win.once('tauri://created', async () => {
// 等待 webview 加载后推送初始状态
setTimeout(() => pushOsdState(), 300)
// 监听窗口移动,保存像素位置并同步更新百分比(拖动结束后触发)
try {
const winInstance = await win
const unlisten = await winInstance.onMoved(async ({ payload }) => {
osdConfig.value.overlayX = payload.x
osdConfig.value.overlayY = payload.y
// 反算百分比:xPct = x / availW * 100availW = screenW - windowW
// 置 suppressPercentWatch=true 避免百分比变化触发 resetOverlayPosition 循环
suppressPercentWatch = true
try {
const monitor = await currentMonitor()
const screenW = (monitor?.size.width ?? 1920) / (monitor?.scaleFactor ?? 1)
const screenH = (monitor?.size.height ?? 1080) / (monitor?.scaleFactor ?? 1)
const size = await winInstance.outerSize()
const scale = monitor?.scaleFactor ?? 1
const winW = size.width / scale
const winH = size.height / scale
const availW = Math.max(1, screenW - winW)
const availH = Math.max(1, screenH - winH)
osdConfig.value.positionXPct = Math.round((payload.x / availW) * 100)
osdConfig.value.positionYPct = Math.round((payload.y / availH) * 100)
} catch { /* 忽略百分比反算失败 */ }
saveOsdConfig(osdConfig.value)
// 下一个微任务后解除抑制(让本次 watch 回调跳过即可)
queueMicrotask(() => { suppressPercentWatch = false })
})
osdEventUnlisteners.push(unlisten)
} catch { /* 忽略 */ }
})
win.once('tauri://error', (e: unknown) => {
console.error('[OSD] 悬浮窗创建失败:', e)
toast.error('悬浮窗创建失败')
})
}
/** 隐藏悬浮窗 */
async function hideOverlayWindow() {
const existing = await WebviewWindow.getByLabel(OSD_OVERLAY_LABEL)
if (existing) {
await existing.hide()
}
}
/** 根据当前配置更新悬浮窗窗口尺寸(显示项数量/布局/字号变化时调用) */
async function updateOsdWindowSize() {
try {
const existing = await WebviewWindow.getByLabel(OSD_OVERLAY_LABEL)
if (!existing) return
const hasNetItem = osdConfig.value.overlayItems.some(i => i.special === 'net-up' || i.special === 'net-down')
const { w, h } = computeOsdWindowSize(
osdConfig.value.overlayItems.length,
osdConfig.value.layout,
osdConfig.value.fontSize,
hasNetItem,
osdConfig.value.overlayItems,
)
await existing.setSize(new LogicalSize(w, h))
} catch { /* 忽略 */ }
}
/** 重置悬浮窗位置到默认(百分比位置),清除保存的像素位置
* 仅重新定位,不改变尺寸——尺寸由悬浮窗内容实际测量上报维持 */
async function resetOverlayPosition() {
osdConfig.value.overlayX = null
osdConfig.value.overlayY = null
saveOsdConfig(osdConfig.value)
// 重新定位窗口
try {
const existing = await WebviewWindow.getByLabel(OSD_OVERLAY_LABEL)
if (existing) {
const monitor = await currentMonitor()
const screenW = (monitor?.size.width ?? 1920) / (monitor?.scaleFactor ?? 1)
const screenH = (monitor?.size.height ?? 1080) / (monitor?.scaleFactor ?? 1)
// 读取窗口当前实际尺寸用于定位计算,不调用 setSize(避免覆盖实际测量值)
const size = await existing.outerSize()
const scale = monitor?.scaleFactor ?? 1
const w = size.width / scale
const h = size.height / scale
const pos = computePositionFromPct(screenW, screenH, w, h, osdConfig.value.positionXPct, osdConfig.value.positionYPct)
await existing.setPosition(new LogicalPosition(pos.x, pos.y))
}
} catch { /* 忽略 */ }
}
// ===== OSD 窗口事件监听 =====
let osdEventUnlisteners: UnlistenFn[] = []
async function setupOsdEventListeners() {
// 守卫:避免重复注册(MonitorModule 可能因预渲染多次挂载)
if (osdEventUnlisteners.length) return
const { listen: tauriListen } = await import('@tauri-apps/api/event')
// 监听悬浮窗上报的实际内容尺寸,按内容调整窗口大小(替代不准确的估算)
// 仅当尺寸变化超过 1px 时才 setSize,避免无意义的频繁调用
let lastW = 0
let lastH = 0
const unlisten = await tauriListen<{ width: number; height: number }>('osd-content-size', async (e) => {
const { width, height } = e.payload
if (Math.abs(width - lastW) < 1 && Math.abs(height - lastH) < 1) return
lastW = width
lastH = height
try {
const w = await WebviewWindow.getByLabel(OSD_OVERLAY_LABEL)
if (w) await w.setSize(new LogicalSize(width, height))
} catch { /* 忽略 */ }
})
osdEventUnlisteners.push(unlisten)
}
// ===== OSD 窗口管理(由 store.initOsd()/ensureOverlayWindow() 等统一管理 =====
// ===== 颜色主题编辑 Dialog =====
const colorThemeDialogOpen = ref(false)
@@ -1570,7 +1041,7 @@ function updateAlertConfig(field: keyof AlertConfig | 'maxValues', value: unknow
if (field === 'maxValues' && maxKey) {
osdConfig.value.alert.maxValues[maxKey] = Number(value)
} else {
;(osdConfig.value.alert as Record<string, unknown>)[field] = value
;(osdConfig.value.alert as unknown as Record<string, unknown>)[field] = value
}
saveOsdConfig(osdConfig.value)
}
@@ -1622,43 +1093,17 @@ onMounted(async () => {
try { appDataPath.value = await appDataDir() } catch { /* 忽略 */ }
store.init()
// 注册 OSD 窗口事件监听
setupOsdEventListeners().catch(e => console.error('[OSD] 事件监听注册失败:', e))
// 初始化悬浮窗(如果开关已开启)
if (osdConfig.value.overlayEnabled) {
ensureOverlayWindow().catch(e => console.error('[OSD] 初始化悬浮窗失败:', e))
}
// 监听托盘菜单"切换 OSD"事件
try {
osdEventUnlisteners.push(
await listen('tray:toggle-osd', () => {
osdConfig.value.overlayEnabled = !osdConfig.value.overlayEnabled
saveOsdConfig(osdConfig.value)
if (osdConfig.value.overlayEnabled) {
if (osdConfig.value.overlayItems.length === 0) {
toast.warning('OSD 显示项为空,已开启但未创建窗口')
} else {
ensureOverlayWindow().catch(e => console.error('[OSD] 托盘开启悬浮窗失败:', e))
}
} else {
hideOverlayWindow().catch(e => console.error('[OSD] 托盘关闭悬浮窗失败:', e))
}
})
)
} catch (e) {
console.error('[OSD] 注册 tray:toggle-osd 监听失败:', e)
}
// OSD 配置/窗口/事件监听已迁移至 monitorStore,由 initOsd() 统一初始化
// (幂等:App 启动时已调用过则跳过,模块挂载时再次调用安全)
store.initOsd()
})
onUnmounted(() => {
// 不 dispose storeSSE 订阅保持,确保切走监控模块后 OSD 仍有数据
// store.subscribe() 内部有守卫(if unlistenFns.length return),重复 init 不会重复订阅
// 不关闭 OSD 窗口:切走监控模块时保持 OSD 显示,由模块禁用或应用退出时统一清理
// 仅清理组件级 OSD 事件监听(下次挂载会重新注册setupOsdEventListeners 有守卫
osdEventUnlisteners.forEach(fn => fn())
osdEventUnlisteners = []
// 释放 OSD 事件监听(App 启动或模块重新挂载会重新注册)
store.disposeOsd()
})
// 当 Kernel 从未就绪变成就绪时,主动拉一次快照填充 UI
@@ -1668,71 +1113,8 @@ watch(() => store.status?.ready, (ready, prev) => {
}
})
// ===== OSD 开关变化时创建/隐藏悬浮窗 =====
watch(() => osdConfig.value.overlayEnabled, (enabled) => {
if (enabled) {
// 开启时若显示项为空则不创建窗口
if (osdConfig.value.overlayItems.length === 0) return
ensureOverlayWindow().catch(e => console.error('[OSD] 创建悬浮窗失败:', e))
} else {
hideOverlayWindow().catch(e => console.error('[OSD] 隐藏悬浮窗失败:', e))
}
})
// ===== 显示项变化时:无项则隐藏窗口,有项且开关开则确保窗口存在 =====
watch(() => osdConfig.value.overlayItems.length, (len) => {
if (!osdConfig.value.overlayEnabled) return
if (len === 0) {
hideOverlayWindow().catch(e => console.error('[OSD] 显示项为空,隐藏悬浮窗失败:', e))
} else {
ensureOverlayWindow().catch(e => console.error('[OSD] 显示项恢复,创建悬浮窗失败:', e))
}
})
// ===== 位置百分比变化时重新定位窗口(清除已保存像素位置) =====
// 拖动 OSD 触发的 onMoved 会反算更新百分比,此时 suppressPercentWatch=true 跳过,避免循环
watch(() => [osdConfig.value.positionXPct, osdConfig.value.positionYPct], () => {
if (suppressPercentWatch) return
// 清除保存的像素位置,让窗口使用百分比重新定位
osdConfig.value.overlayX = null
osdConfig.value.overlayY = null
saveOsdConfig(osdConfig.value)
// 如果窗口已存在,重新定位
resetOverlayPosition().catch(() => {})
})
// ===== 数据变化时推送状态到 OSD 窗口 =====
// 快照变化(Kernel SSE 推送)→ 推送到 OSD 窗口
watch(() => store.snapshot, () => {
if (osdConfig.value.overlayEnabled) {
pushOsdState()
}
}, { deep: false })
// 网速变化 → 推送到 OSD 窗口
watch(() => store.networkSpeed, () => {
if (osdConfig.value.overlayEnabled) {
pushOsdState()
}
}, { deep: false })
// OSD 配置变化 → 推送到 OSD 窗口(位置/字体/显示项等)
watch(osdConfig, () => {
if (osdConfig.value.overlayEnabled) {
pushOsdState()
}
}, { deep: true })
// 显示项数量/布局/字号变化 → 更新悬浮窗窗口尺寸(自适应内容)
watch([
() => osdConfig.value.overlayItems.length,
() => osdConfig.value.layout,
() => osdConfig.value.fontSize,
], () => {
if (osdConfig.value.overlayEnabled) {
updateOsdWindowSize().catch(() => {})
}
})
// OSD 相关 watch(开关/显示项/位置/配置/尺寸)已由 store.initOsd() 内部统一注册,
// 与组件生命周期解耦:模块卸载后 OSD 仍能持续刷新,配置变更仍会推送。
</script>
<template>