监控、OSD
This commit is contained in:
@@ -131,6 +131,11 @@ internal sealed class HardwareManager : IDisposable
|
|||||||
|
|
||||||
// 重新遍历以读取最新传感器值(visitor 缓存的是 hardware 引用,sensor 值实时)
|
// 重新遍历以读取最新传感器值(visitor 缓存的是 hardware 引用,sensor 值实时)
|
||||||
var groups = new Dictionary<string, SensorGroup>();
|
var groups = new Dictionary<string, SensorGroup>();
|
||||||
|
|
||||||
|
// 为已启用但 LHB 未枚举到的硬件类型预创建空分组,
|
||||||
|
// 确保前端能显示"已启用但无数据"的硬件(而不是直接隐藏,让用户误以为配置未生效)
|
||||||
|
EnsureGroupsForEnabledHardware(groups, snap);
|
||||||
|
|
||||||
foreach (var hw in _visitor.AllHardware)
|
foreach (var hw in _visitor.AllHardware)
|
||||||
{
|
{
|
||||||
string groupId = hw.HardwareType.ToString().ToLowerInvariant();
|
string groupId = hw.HardwareType.ToString().ToLowerInvariant();
|
||||||
@@ -164,6 +169,40 @@ internal sealed class HardwareManager : IDisposable
|
|||||||
return snap;
|
return snap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为已启用但 LHB 未枚举到的硬件类型预创建空分组。
|
||||||
|
/// 场景:用户在设置中勾选了主板/电池/电源等,但 LHB 在当前权限或机型下检测不到对应硬件,
|
||||||
|
/// 此时仍创建空分组让前端显示"已启用但无数据",避免用户误以为配置未生效。
|
||||||
|
/// GPU 特殊处理:LHB 会枚举到 GpuIntel/GpuAmd/GpuNvidia 之一,不预创建通用分组。
|
||||||
|
/// </summary>
|
||||||
|
private void EnsureGroupsForEnabledHardware(Dictionary<string, SensorGroup> groups, SensorSnapshot snap)
|
||||||
|
{
|
||||||
|
// (configKey, groupId, groupName)
|
||||||
|
// 注意:controller 对应 SuperIO 和 EmbeddedController 两个 HardwareType
|
||||||
|
var mapping = new (string, string, string)[]
|
||||||
|
{
|
||||||
|
("cpu", "cpu", "CPU"),
|
||||||
|
("memory", "memory", "Memory"),
|
||||||
|
("storage", "storage", "Storage"),
|
||||||
|
("motherboard", "motherboard", "Motherboard"),
|
||||||
|
("controller", "superio", "SuperIO"),
|
||||||
|
("controller", "embeddedcontroller", "EmbeddedController"),
|
||||||
|
("battery", "battery", "Battery"),
|
||||||
|
("network", "network", "Network"),
|
||||||
|
("psu", "psu", "Psu"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (key, groupId, groupName) in mapping)
|
||||||
|
{
|
||||||
|
if (_config.IsHardwareEnabled(key) && !groups.ContainsKey(groupId))
|
||||||
|
{
|
||||||
|
var g = new SensorGroup { Id = groupId, Name = groupName };
|
||||||
|
groups[groupId] = g;
|
||||||
|
snap.Groups.Add(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 判断硬件是否属于慢通道(低频 Update 即可)。
|
/// 判断硬件是否属于慢通道(低频 Update 即可)。
|
||||||
/// 快通道:CPU/GPU/Memory/Network(变化快、查询轻量)
|
/// 快通道:CPU/GPU/Memory/Network(变化快、查询轻量)
|
||||||
|
|||||||
+10
-1
@@ -37,7 +37,16 @@ sysinfo = "0.32"
|
|||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
windows-sys = { version = "0.52", features = ["Win32_Networking_WinInet", "Win32_Foundation", "Win32_Security", "Win32_System_Threading"] }
|
windows-sys = { version = "0.52", features = [
|
||||||
|
"Win32_Networking_WinInet",
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_UI_HiDpi",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
] }
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2"
|
tauri-plugin-autostart = "2"
|
||||||
|
|||||||
Binary file not shown.
@@ -20,6 +20,12 @@
|
|||||||
"core:window:allow-set-effects",
|
"core:window:allow-set-effects",
|
||||||
"core:window:allow-set-background-color",
|
"core:window:allow-set-background-color",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:webview:allow-create-webview-window",
|
||||||
|
"core:window:allow-set-position",
|
||||||
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-set-always-on-top",
|
||||||
|
"core:window:allow-set-skip-taskbar",
|
||||||
|
"core:window:allow-set-decorations",
|
||||||
"snap-layout:default"
|
"snap-layout:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "osd",
|
||||||
|
"description": "Capability for OSD overlay window",
|
||||||
|
"windows": ["osd-overlay"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:window:allow-hide",
|
||||||
|
"core:window:allow-show",
|
||||||
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-set-position",
|
||||||
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-set-always-on-top",
|
||||||
|
"core:window:allow-set-skip-taskbar",
|
||||||
|
"core:window:allow-set-decorations",
|
||||||
|
"core:window:allow-set-ignore-cursor-events",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:event:allow-emit",
|
||||||
|
"core:event:allow-listen",
|
||||||
|
"snap-layout:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ mod logger;
|
|||||||
mod mihomo_manager;
|
mod mihomo_manager;
|
||||||
mod monitor_kernel;
|
mod monitor_kernel;
|
||||||
mod network_monitor;
|
mod network_monitor;
|
||||||
|
mod osd_window;
|
||||||
mod process_manager;
|
mod process_manager;
|
||||||
|
|
||||||
use download_engine::{
|
use download_engine::{
|
||||||
@@ -31,6 +32,10 @@ use monitor_kernel::{
|
|||||||
monitor_stop, MonitorKernel,
|
monitor_stop, MonitorKernel,
|
||||||
};
|
};
|
||||||
use network_monitor::{network_monitor_status, NetworkMonitor};
|
use network_monitor::{network_monitor_status, NetworkMonitor};
|
||||||
|
use osd_window::{
|
||||||
|
osd_apply_overlay_style, osd_begin_drag, osd_set_click_through, osd_set_topmost,
|
||||||
|
osd_start_drag_watch, osd_start_topmost_watch, osd_stop_watch,
|
||||||
|
};
|
||||||
use process_manager::{
|
use process_manager::{
|
||||||
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
|
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
|
||||||
stop_all_processes, stop_process, ProcessManager,
|
stop_all_processes, stop_process, ProcessManager,
|
||||||
@@ -122,6 +127,13 @@ pub fn run() {
|
|||||||
monitor_get_hardware_config,
|
monitor_get_hardware_config,
|
||||||
monitor_set_hardware_config,
|
monitor_set_hardware_config,
|
||||||
network_monitor_status,
|
network_monitor_status,
|
||||||
|
osd_apply_overlay_style,
|
||||||
|
osd_begin_drag,
|
||||||
|
osd_set_click_through,
|
||||||
|
osd_set_topmost,
|
||||||
|
osd_start_drag_watch,
|
||||||
|
osd_start_topmost_watch,
|
||||||
|
osd_stop_watch,
|
||||||
downloader_get_tasks,
|
downloader_get_tasks,
|
||||||
downloader_add_task,
|
downloader_add_task,
|
||||||
downloader_pause_task,
|
downloader_pause_task,
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
//! OSD 悬浮窗 Windows 原生窗口管理
|
||||||
|
//!
|
||||||
|
//! 提供:
|
||||||
|
//! - NoActivate(不获取焦点):设置 WS_EX_NOACTIVATE 扩展样式
|
||||||
|
//! - 右键长按拖动:轮询 GetAsyncKeyState 检测右键长按,触发拖动事件
|
||||||
|
//! - 任务栏覆盖检测:轮询 GetForegroundWindow,检测系统 UI 出现时暂时取消置顶
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
/// 右键拖动监视线程停止标志(全局,仅一个 OSD 悬浮窗)
|
||||||
|
static DRAG_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
||||||
|
/// 任务栏覆盖监视线程停止标志
|
||||||
|
static TOPMOST_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn drag_stop() -> &'static Arc<AtomicBool> {
|
||||||
|
DRAG_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn topmost_stop() -> &'static Arc<AtomicBool> {
|
||||||
|
TOPMOST_STOP.get_or_init(|| Arc::new(AtomicBool::new(true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod win_api {
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use windows_sys::Win32::Foundation::{POINT, RECT};
|
||||||
|
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_RBUTTON};
|
||||||
|
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||||
|
GetClassNameW, GetCursorPos, GetForegroundWindow, GetWindowLongPtrW,
|
||||||
|
GetWindowRect, SendMessageW, SetWindowLongPtrW, SetWindowPos,
|
||||||
|
GWL_EXSTYLE, HTCAPTION, HWND_NOTOPMOST, HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE,
|
||||||
|
SWP_NOSIZE, SWP_SHOWWINDOW, WM_NCLBUTTONDOWN, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
|
||||||
|
WS_EX_TRANSPARENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// windows-sys 的 HWND 类型别名(isize)
|
||||||
|
pub type Hwnd = isize;
|
||||||
|
|
||||||
|
/// 从 Tauri 窗口标签获取原生 HWND
|
||||||
|
pub fn get_hwnd(label: &str, app: &AppHandle) -> Option<Hwnd> {
|
||||||
|
let win = app.get_webview_window(label)?;
|
||||||
|
let hwnd = win.hwnd().ok()?;
|
||||||
|
// Tauri 的 hwnd() 返回 windows::Win32::Foundation::HWND(pub *mut c_void)
|
||||||
|
// 转为 isize 用于 windows-sys 调用
|
||||||
|
Some(hwnd.0 as isize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置窗口扩展样式:WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW
|
||||||
|
pub fn apply_no_activate(hwnd: Hwnd) {
|
||||||
|
unsafe {
|
||||||
|
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
||||||
|
let new_ex = ex | (WS_EX_NOACTIVATE as isize) | (WS_EX_TOOLWINDOW as isize);
|
||||||
|
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置点击穿透(WS_EX_TRANSPARENT)
|
||||||
|
pub fn set_click_through(hwnd: Hwnd, enabled: bool) {
|
||||||
|
unsafe {
|
||||||
|
let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
||||||
|
let new_ex = if enabled {
|
||||||
|
ex | (WS_EX_TRANSPARENT as isize)
|
||||||
|
} else {
|
||||||
|
ex & !(WS_EX_TRANSPARENT as isize)
|
||||||
|
};
|
||||||
|
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取鼠标光标位置
|
||||||
|
pub fn get_cursor_pos() -> Option<POINT> {
|
||||||
|
let mut pt = POINT { x: 0, y: 0 };
|
||||||
|
unsafe {
|
||||||
|
if GetCursorPos(&mut pt) != 0 {
|
||||||
|
Some(pt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取窗口矩形
|
||||||
|
pub fn get_window_rect(hwnd: Hwnd) -> Option<RECT> {
|
||||||
|
let mut rect = RECT {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
if GetWindowRect(hwnd, &mut rect) != 0 {
|
||||||
|
Some(rect)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断点是否在窗口矩形内
|
||||||
|
pub fn point_in_rect(pt: POINT, rect: RECT) -> bool {
|
||||||
|
pt.x >= rect.left && pt.x <= rect.right && pt.y >= rect.top && pt.y <= rect.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断右键是否按下
|
||||||
|
pub fn is_rbutton_down() -> bool {
|
||||||
|
// GetAsyncKeyState 返回 i16,最高位为 1 表示按下
|
||||||
|
// 转 u16 后与 0x8000 进行按位与,避免 i16 字面量溢出
|
||||||
|
unsafe { (GetAsyncKeyState(VK_RBUTTON as i32) as u16 & 0x8000) != 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取前景窗口
|
||||||
|
pub fn get_foreground_window() -> Hwnd {
|
||||||
|
unsafe { GetForegroundWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取窗口类名
|
||||||
|
pub fn get_class_name(hwnd: Hwnd) -> Option<String> {
|
||||||
|
let mut buf = [0u16; 256];
|
||||||
|
unsafe {
|
||||||
|
let len = GetClassNameW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
|
||||||
|
if len > 0 {
|
||||||
|
Some(String::from_utf16_lossy(&buf[..len as usize]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置窗口置顶状态
|
||||||
|
pub fn set_topmost(hwnd: Hwnd, topmost: bool) {
|
||||||
|
unsafe {
|
||||||
|
let insert_after = if topmost {
|
||||||
|
HWND_TOPMOST
|
||||||
|
} else {
|
||||||
|
HWND_NOTOPMOST
|
||||||
|
};
|
||||||
|
SetWindowPos(
|
||||||
|
hwnd,
|
||||||
|
insert_after,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动原生拖动:发送 WM_NCLBUTTONDOWN(HTCAPTION) 进入标题栏拖动
|
||||||
|
///
|
||||||
|
/// 调用前应已关闭 WS_EX_TRANSPARENT(点击穿透),否则窗口无法接收后续鼠标事件。
|
||||||
|
/// SendMessageW 会阻塞直到拖动结束,应在独立线程调用。
|
||||||
|
pub fn begin_drag(hwnd: Hwnd) {
|
||||||
|
unsafe {
|
||||||
|
// 获取当前鼠标位置,编码为 LPARAM(低字 x,高字 y)
|
||||||
|
let pt = get_cursor_pos().unwrap_or(POINT { x: 0, y: 0 });
|
||||||
|
let lparam = ((pt.y as isize) << 16) | (pt.x as isize & 0xFFFF);
|
||||||
|
SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION as usize, lparam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断窗口类名是否为系统 UI(任务栏、开始菜单、通知区域等)
|
||||||
|
pub fn is_system_ui_class(class_name: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
class_name,
|
||||||
|
"Shell_TrayWnd" // 任务栏
|
||||||
|
| "Shell_SecondaryTrayWnd" // 副任务栏
|
||||||
|
| "Windows.UI.Core.CoreWindow" // UWP 系统弹窗(开始菜单、通知中心、日历等)
|
||||||
|
| "XamlExplorerHostIslandWindow" // Win11 系统弹窗容器
|
||||||
|
| "TopLevelWindowForOverflowSink" // Win11 系统托盘溢出
|
||||||
|
| "TopLevelWindowForHiddenRegion" // Win11 系统隐藏区域
|
||||||
|
| "Windows.UI.Shell.ShellFlyoutWindow" // Win11 Shell 弹出
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用 OSD 悬浮窗的原生样式(NoActivate + ToolWindow)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_apply_overlay_style(label: String, app: AppHandle) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let hwnd = win_api::get_hwnd(&label, &app)
|
||||||
|
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
|
||||||
|
win_api::apply_no_activate(hwnd);
|
||||||
|
eprintln!("[osd] 已应用 NoActivate 样式到窗口 {}", label);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动右键长按拖动监视
|
||||||
|
///
|
||||||
|
/// 轮询检测右键按下,持续超过 400ms 且鼠标在 OSD 窗口上方时,
|
||||||
|
/// 发出 `osd-start-drag` 事件通知前端临时关闭点击穿透并开始拖动。
|
||||||
|
/// 右键释放后发出 `osd-end-drag` 事件。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_start_drag_watch(label: String, app: AppHandle) -> Result<(), String> {
|
||||||
|
// 停止旧线程,等待退出
|
||||||
|
drag_stop().store(true, Ordering::SeqCst);
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
let stop_flag = drag_stop().clone();
|
||||||
|
stop_flag.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let app_handle = app.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut rbutton_was_down = false;
|
||||||
|
let mut press_start: Option<Instant> = None;
|
||||||
|
let mut drag_emitted = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if stop_flag.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let rbutton_down = win_api::is_rbutton_down();
|
||||||
|
|
||||||
|
if rbutton_down && !rbutton_was_down {
|
||||||
|
// 右键刚按下
|
||||||
|
press_start = Some(Instant::now());
|
||||||
|
drag_emitted = false;
|
||||||
|
} else if rbutton_down && rbutton_was_down {
|
||||||
|
// 右键持续按住,检测长按
|
||||||
|
if !drag_emitted {
|
||||||
|
if let Some(start) = press_start {
|
||||||
|
if start.elapsed() >= Duration::from_millis(400) {
|
||||||
|
if let Some(hwnd) = win_api::get_hwnd(&label, &app_handle) {
|
||||||
|
if let (Some(pt), Some(rect)) =
|
||||||
|
(win_api::get_cursor_pos(), win_api::get_window_rect(hwnd))
|
||||||
|
{
|
||||||
|
if win_api::point_in_rect(pt, rect) {
|
||||||
|
let _ = app_handle.emit("osd-start-drag", ());
|
||||||
|
drag_emitted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !rbutton_down && rbutton_was_down {
|
||||||
|
// 右键释放
|
||||||
|
if drag_emitted {
|
||||||
|
let _ = app_handle.emit("osd-end-drag", ());
|
||||||
|
}
|
||||||
|
press_start = None;
|
||||||
|
drag_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rbutton_was_down = rbutton_down;
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(30));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动任务栏覆盖监视
|
||||||
|
///
|
||||||
|
/// 轮询检测前景窗口变化,当系统 UI(任务栏弹窗、开始菜单等)变为前景时,
|
||||||
|
/// 发出 `osd-system-ui-active` 事件通知前端暂时取消置顶;
|
||||||
|
/// 系统 UI 关闭后发出 `osd-system-ui-inactive` 事件恢复置顶。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_start_topmost_watch(app: AppHandle) -> Result<(), String> {
|
||||||
|
topmost_stop().store(true, Ordering::SeqCst);
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
let stop_flag = topmost_stop().clone();
|
||||||
|
stop_flag.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let app_handle = app.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut last_foreground: isize = 0;
|
||||||
|
let mut system_ui_active = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if stop_flag.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let fg = win_api::get_foreground_window();
|
||||||
|
if fg != last_foreground {
|
||||||
|
last_foreground = fg;
|
||||||
|
if let Some(class) = win_api::get_class_name(fg) {
|
||||||
|
if win_api::is_system_ui_class(&class) {
|
||||||
|
if !system_ui_active {
|
||||||
|
system_ui_active = true;
|
||||||
|
let _ = app_handle.emit("osd-system-ui-active", ());
|
||||||
|
}
|
||||||
|
} else if system_ui_active {
|
||||||
|
system_ui_active = false;
|
||||||
|
let _ = app_handle.emit("osd-system-ui-inactive", ());
|
||||||
|
}
|
||||||
|
} else if system_ui_active {
|
||||||
|
system_ui_active = false;
|
||||||
|
let _ = app_handle.emit("osd-system-ui-inactive", ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(150));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止所有 OSD 监视线程
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_stop_watch() {
|
||||||
|
drag_stop().store(true, Ordering::SeqCst);
|
||||||
|
topmost_stop().store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置点击穿透(Rust 侧原生 WS_EX_TRANSPARENT,比 JS setIgnoreCursorEvents 更可靠)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_set_click_through(label: String, enabled: bool, app: AppHandle) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let hwnd = win_api::get_hwnd(&label, &app)
|
||||||
|
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
|
||||||
|
win_api::set_click_through(hwnd, enabled);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置窗口置顶状态(Rust 侧原生 SetWindowPos)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_set_topmost(label: String, topmost: bool, app: AppHandle) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let hwnd = win_api::get_hwnd(&label, &app)
|
||||||
|
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
|
||||||
|
win_api::set_topmost(hwnd, topmost);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动原生拖动(右键长按触发)
|
||||||
|
///
|
||||||
|
/// 同步关闭点击穿透(WS_EX_TRANSPARENT),然后在独立线程中调用
|
||||||
|
/// `SendMessage(WM_NCLBUTTONDOWN, HTCAPTION)` 进入原生标题栏拖动模式。
|
||||||
|
/// 相比前端 `startDragging()`,此方式在 WS_EX_NOACTIVATE 窗口上更可靠。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn osd_begin_drag(label: String, app: AppHandle) -> Result<(), String> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let hwnd = win_api::get_hwnd(&label, &app)
|
||||||
|
.ok_or_else(|| format!("窗口 {} 不存在", label))?;
|
||||||
|
// 先同步关闭穿透,确保窗口能接收后续鼠标事件
|
||||||
|
win_api::set_click_through(hwnd, false);
|
||||||
|
// SendMessageW 会阻塞直到拖动结束,在独立线程中调用避免阻塞 IPC
|
||||||
|
thread::spawn(move || {
|
||||||
|
win_api::begin_drag(hwnd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+19
-4
@@ -1,12 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import 'vue-sonner/style.css'
|
import 'vue-sonner/style.css'
|
||||||
|
|
||||||
// 导入模块注册入口 —— 副作用导入,注册所有模块到 moduleRegistry
|
|
||||||
import './modules'
|
|
||||||
|
|
||||||
import { createLogger } from './lib/logger'
|
import { createLogger } from './lib/logger'
|
||||||
const logger = createLogger('main')
|
const logger = createLogger('main')
|
||||||
|
|
||||||
@@ -19,6 +15,22 @@ window.addEventListener('unhandledrejection', (event) => {
|
|||||||
logger.error(`未处理的Promise拒绝: ${event.reason}`)
|
logger.error(`未处理的Promise拒绝: ${event.reason}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ===== OSD 窗口模式检测 =====
|
||||||
|
// 通过 URL hash 识别:#osd-overlay(悬浮窗)
|
||||||
|
// OSD 窗口是精简的独立 Vue 应用,不加载主应用的 store 和模块
|
||||||
|
const osdHash = window.location.hash
|
||||||
|
if (osdHash === '#osd-overlay') {
|
||||||
|
logger.info(`OSD 窗口启动: ${osdHash}`)
|
||||||
|
void import('./modules/monitor/OsdWindow.vue').then(({ default: OsdWindow }) => {
|
||||||
|
const app = createApp(OsdWindow)
|
||||||
|
app.mount('#app')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// ===== 主应用模式 =====
|
||||||
|
void import('./App.vue').then(async ({ default: App }) => {
|
||||||
|
// 导入模块注册入口 —— 副作用导入,注册所有模块到 moduleRegistry
|
||||||
|
await import('./modules')
|
||||||
|
|
||||||
logger.info('Thing 应用启动')
|
logger.info('Thing 应用启动')
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@@ -47,3 +59,6 @@ void import('./stores/searchStore').then(({ useSearchStore }) => {
|
|||||||
void import('./stores/processStore').then(({ useProcessStore }) => {
|
void import('./stores/processStore').then(({ useProcessStore }) => {
|
||||||
useProcessStore().initListener().catch(e => console.error('Process listener init error:', e))
|
useProcessStore().initListener().catch(e => console.error('Process listener init error:', e))
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,775 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
|
import { listen, emit, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
// ===== 数据契约(与主窗口 MonitorModule 共享,此处独立声明避免循环依赖) =====
|
||||||
|
interface OsdItem {
|
||||||
|
key: string
|
||||||
|
groupId: string
|
||||||
|
sensorName: string
|
||||||
|
hardwareName: string
|
||||||
|
type: string
|
||||||
|
unit: string
|
||||||
|
special?: 'net-up' | 'net-down'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColorTheme {
|
||||||
|
hardware: Record<string, string>
|
||||||
|
sensor: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 警告色配置:阈值百分比 + 警告/严重颜色 */
|
||||||
|
interface AlertConfig {
|
||||||
|
/** 警告色开关 */
|
||||||
|
enabled: boolean
|
||||||
|
/** 警告阈值百分比(达到即变警告色,如 80) */
|
||||||
|
warnThreshold: number
|
||||||
|
/** 严重阈值百分比(达到即变严重色,如 90) */
|
||||||
|
criticalThreshold: number
|
||||||
|
/** 警告色(淡红,hex) */
|
||||||
|
warnColor: string
|
||||||
|
/** 严重色(大红,hex) */
|
||||||
|
criticalColor: string
|
||||||
|
/** 各硬件类型的最大值(用于将温度等非百分比值转为百分比)
|
||||||
|
* CPU 温度墙默认 100,GPU 默认 85 */
|
||||||
|
maxValues: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OsdConfig {
|
||||||
|
overlayEnabled: boolean
|
||||||
|
overlayItems: OsdItem[]
|
||||||
|
positionXPct: number
|
||||||
|
positionYPct: number
|
||||||
|
overlayOpacity: number
|
||||||
|
fontSize: number
|
||||||
|
showUnit: boolean
|
||||||
|
showLabel: boolean
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SensorEntry {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
hardwareName: string
|
||||||
|
value: number | null
|
||||||
|
unit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SensorGroup {
|
||||||
|
id: string
|
||||||
|
sensors: SensorEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SensorSnapshot {
|
||||||
|
groups: SensorGroup[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NetworkSpeed {
|
||||||
|
downloadBps: number
|
||||||
|
uploadBps: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OsdStatePayload {
|
||||||
|
config: OsdConfig
|
||||||
|
snapshot: SensorSnapshot | null
|
||||||
|
networkSpeed: NetworkSpeed | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 默认颜色主题(小飞机风格) =====
|
||||||
|
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: false,
|
||||||
|
warnThreshold: 80,
|
||||||
|
criticalThreshold: 90,
|
||||||
|
warnColor: '#FF6B6B',
|
||||||
|
criticalColor: '#FF0000',
|
||||||
|
maxValues: {
|
||||||
|
cpu: 100,
|
||||||
|
gpu: 85,
|
||||||
|
gpuintel: 85,
|
||||||
|
gpuamd: 85,
|
||||||
|
gpunvidia: 85,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算某项的警告百分比(0-100)
|
||||||
|
* 百分比类传感器(load/level)直接用值;温度等需除以该硬件类型的最大值 */
|
||||||
|
function itemAlertPct(item: OsdItem, value: number): number {
|
||||||
|
if (value == null || !isFinite(value)) return 0
|
||||||
|
// 百分比类传感器直接用值
|
||||||
|
if (item.type === 'load' || item.type === 'level') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
// 温度等需除以该硬件类型的最大值
|
||||||
|
if (item.type === 'temperature') {
|
||||||
|
const alert = config.value?.alert ?? DEFAULT_ALERT_CONFIG
|
||||||
|
// GPU 系列合并查找
|
||||||
|
let gkey = item.groupId
|
||||||
|
if (gkey.startsWith('gpu')) gkey = 'gpu'
|
||||||
|
const max = alert.maxValues[gkey] ?? alert.maxValues[item.groupId] ?? 100
|
||||||
|
return (value / max) * 100
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取项的警告色(若达到阈值),否则返回 null */
|
||||||
|
function itemAlertColor(item: OsdItem): string | null {
|
||||||
|
const alert = config.value?.alert
|
||||||
|
if (!alert?.enabled) return null
|
||||||
|
const value = getOsdItemValue(item)
|
||||||
|
if (value == null || !isFinite(value)) return null
|
||||||
|
const pct = itemAlertPct(item, value)
|
||||||
|
if (pct >= alert.criticalThreshold) return alert.criticalColor
|
||||||
|
if (pct >= alert.warnThreshold) return alert.warnColor
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 标签生成(根据 labelLanguage 返回中文通俗标题或英文传感器名) =====
|
||||||
|
|
||||||
|
/** 是否英文模式 */
|
||||||
|
function isEn(): boolean {
|
||||||
|
return config.value?.labelLanguage === 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 分组(group / multiline / single 布局用) =====
|
||||||
|
|
||||||
|
interface OsdGroup {
|
||||||
|
/** 分组 key:cpu / gpu / memory / storage / network / ... */
|
||||||
|
key: string
|
||||||
|
/** 分组标题:CPU / GPU / 内存 / 网络 ... */
|
||||||
|
label: string
|
||||||
|
items: OsdItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将显示项按硬件类型分组(GPU 系列合并为 gpu,网速特殊项归入 network) */
|
||||||
|
function groupOsdItems(items: OsdItem[]): OsdGroup[] {
|
||||||
|
const groups: OsdGroup[] = []
|
||||||
|
const map = new Map<string, OsdGroup>()
|
||||||
|
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) {
|
||||||
|
g = { key: gkey, label: groupLabel(gkey), items: [] }
|
||||||
|
map.set(gkey, g)
|
||||||
|
groups.push(g)
|
||||||
|
}
|
||||||
|
g.items.push(item)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分组标题 */
|
||||||
|
function groupLabel(gkey: string): string {
|
||||||
|
if (isEn()) {
|
||||||
|
switch (gkey) {
|
||||||
|
case 'cpu': return 'CPU'
|
||||||
|
case 'gpu': return 'GPU'
|
||||||
|
case 'memory': return 'RAM'
|
||||||
|
case 'storage': return 'DISK'
|
||||||
|
case 'network': return 'NET'
|
||||||
|
case 'motherboard': return 'MB'
|
||||||
|
case 'battery': return 'BAT'
|
||||||
|
case 'psu': return 'PSU'
|
||||||
|
default: return gkey.toUpperCase().slice(0, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (gkey) {
|
||||||
|
case 'cpu': return 'CPU'
|
||||||
|
case 'gpu': return 'GPU'
|
||||||
|
case 'memory': return '内存'
|
||||||
|
case 'storage': return '存储'
|
||||||
|
case 'network': return '网络'
|
||||||
|
case 'motherboard': return '主板'
|
||||||
|
case 'battery': return '电池'
|
||||||
|
case 'psu': return '电源'
|
||||||
|
default: return gkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 网络方向箭头 */
|
||||||
|
function netArrow(item: OsdItem): string {
|
||||||
|
if (item.special === 'net-up') return '↑'
|
||||||
|
if (item.special === 'net-down') return '↓'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 固定宽度格式化(group / multiline 布局用,避免数据变动导致宽度跳动) =====
|
||||||
|
|
||||||
|
/** 右对齐补空格到指定宽度 */
|
||||||
|
function padNum(s: string, width: number): string {
|
||||||
|
return s.length >= width ? s : ' '.repeat(width - s.length) + s
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 固定宽度数值(不含单位),按传感器类型决定小数位和宽度 */
|
||||||
|
function fmtFixedValue(v: number | null, item: OsdItem): string {
|
||||||
|
if (v == null || !isFinite(v)) return '--'
|
||||||
|
if (item.special === 'net-up' || item.special === 'net-down') {
|
||||||
|
// 分割显示的数值部分:6 字符宽度
|
||||||
|
if (v >= 1_048_576) return padNum((v / 1_048_576).toFixed(2), 6)
|
||||||
|
if (v >= 1024) return padNum((v / 1024).toFixed(1), 6)
|
||||||
|
return padNum(v.toFixed(0), 6)
|
||||||
|
}
|
||||||
|
switch (item.type) {
|
||||||
|
case 'load':
|
||||||
|
case 'level':
|
||||||
|
return padNum(Math.round(v).toString(), 3) // 0-100 → 3 字符
|
||||||
|
case 'temperature':
|
||||||
|
return padNum(Math.round(v).toString(), 3) // 0-150 → 3 字符
|
||||||
|
case 'power':
|
||||||
|
case 'voltage':
|
||||||
|
return padNum(v.toFixed(2), 5) // 12.34 → 5 字符
|
||||||
|
case 'clock':
|
||||||
|
case 'frequency':
|
||||||
|
return padNum(Math.round(v).toString(), 4) // 4500 → 4 字符
|
||||||
|
case 'fan':
|
||||||
|
return padNum(Math.round(v).toString(), 4) // 3500 → 4 字符
|
||||||
|
case 'data':
|
||||||
|
case 'smalldata':
|
||||||
|
return padNum(v.toFixed(1), 5) // 1024.0 → 5 字符
|
||||||
|
default:
|
||||||
|
return padNum(v.toFixed(1), 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 固定宽度单位(网速分割显示时返回 4 字符单位) */
|
||||||
|
function fmtFixedUnit(item: OsdItem): string {
|
||||||
|
if (item.special === 'net-up' || item.special === 'net-down') {
|
||||||
|
const v = getOsdItemValue(item)
|
||||||
|
if (v == null || !isFinite(v)) return ' '
|
||||||
|
if (v >= 1_048_576) return 'MB/s'
|
||||||
|
if (v >= 1024) return 'KB/s'
|
||||||
|
return ' B/s'
|
||||||
|
}
|
||||||
|
if (!config.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 || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 状态 =====
|
||||||
|
const config = ref<OsdConfig | null>(null)
|
||||||
|
const snapshot = ref<SensorSnapshot | null>(null)
|
||||||
|
const networkSpeed = ref<NetworkSpeed | null>(null)
|
||||||
|
let unlistenFns: UnlistenFn[] = []
|
||||||
|
|
||||||
|
// ===== 数据查询 =====
|
||||||
|
function getOsdItemValue(item: OsdItem): number | null {
|
||||||
|
if (item.special === 'net-up') return networkSpeed.value?.uploadBps ?? null
|
||||||
|
if (item.special === 'net-down') return networkSpeed.value?.downloadBps ?? null
|
||||||
|
if (!snapshot.value) return null
|
||||||
|
for (const g of snapshot.value.groups) {
|
||||||
|
if (g.id !== item.groupId) continue
|
||||||
|
const s = g.sensors.find(s =>
|
||||||
|
s.hardwareName === item.hardwareName && s.name === item.sensorName && s.type === item.type
|
||||||
|
)
|
||||||
|
if (s) return s.value ?? null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 颜色主题 =====
|
||||||
|
|
||||||
|
/** 将 hex 颜色 + 不透明度(0-100) 转为 rgba 字符串 */
|
||||||
|
function withOpacity(hex: string, opacityPct: number): string {
|
||||||
|
const a = Math.max(0, Math.min(100, opacityPct)) / 100
|
||||||
|
// 解析 #RGB / #RRGGBB / #RRGGBBAA
|
||||||
|
let h = hex.replace('#', '').trim()
|
||||||
|
if (h.length === 3) h = h.split('').map(c => c + c).join('')
|
||||||
|
if (h.length === 8) {
|
||||||
|
// 已含 alpha,先解析再用新 alpha 覆盖
|
||||||
|
h = h.slice(0, 6)
|
||||||
|
}
|
||||||
|
if (h.length !== 6 || /[^0-9a-fA-F]/.test(h)) return hex // 解析失败,原样返回
|
||||||
|
const r = parseInt(h.slice(0, 2), 16)
|
||||||
|
const g = parseInt(h.slice(2, 4), 16)
|
||||||
|
const b = parseInt(h.slice(4, 6), 16)
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemColor(item: OsdItem): string {
|
||||||
|
const opacity = config.value?.fontOpacity ?? 100
|
||||||
|
if (!config.value?.colorThemeEnabled) return withOpacity(config.value?.fontColor ?? '#ffffff', opacity)
|
||||||
|
const theme = config.value.colorTheme ?? DEFAULT_COLOR_THEME
|
||||||
|
// 优先按硬件类型着色
|
||||||
|
const hwColor = theme.hardware[item.groupId]
|
||||||
|
if (hwColor) return withOpacity(hwColor, opacity)
|
||||||
|
// 其次按传感器类型着色
|
||||||
|
const sensorColor = theme.sensor[item.type]
|
||||||
|
if (sensorColor) return withOpacity(sensorColor, opacity)
|
||||||
|
return withOpacity(config.value?.fontColor ?? '#ffffff', opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 项颜色(含警告色优先:达到阈值时覆盖为警告/严重色) */
|
||||||
|
function itemColorWithAlert(item: OsdItem): string {
|
||||||
|
const opacity = config.value?.fontOpacity ?? 100
|
||||||
|
const alertCol = itemAlertColor(item)
|
||||||
|
if (alertCol) return withOpacity(alertCol, opacity)
|
||||||
|
return itemColor(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分组标题警告色:组内任一项达到严重阈值则标题变严重色,达到警告阈值则变警告色,否则用默认色 */
|
||||||
|
function groupLabelColor(g: OsdGroup): string {
|
||||||
|
const opacity = config.value?.fontOpacity ?? 100
|
||||||
|
const alert = config.value?.alert
|
||||||
|
if (!alert?.enabled) return withOpacity(config.value?.fontColor ?? '#ffffff', opacity)
|
||||||
|
let maxPct = 0
|
||||||
|
for (const item of g.items) {
|
||||||
|
const v = getOsdItemValue(item)
|
||||||
|
if (v == null || !isFinite(v)) continue
|
||||||
|
const pct = itemAlertPct(item, v)
|
||||||
|
if (pct > maxPct) maxPct = pct
|
||||||
|
}
|
||||||
|
if (maxPct >= alert.criticalThreshold) return withOpacity(alert.criticalColor, opacity)
|
||||||
|
if (maxPct >= alert.warnThreshold) return withOpacity(alert.warnColor, opacity)
|
||||||
|
return withOpacity(config.value?.fontColor ?? '#ffffff', opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontColor = computed(() => withOpacity(config.value?.fontColor ?? '#ffffff', config.value?.fontOpacity ?? 100))
|
||||||
|
|
||||||
|
/** 字体描边样式(通过 8 方向 text-shadow 实现描边效果) */
|
||||||
|
const fontStrokeStyle = computed(() => {
|
||||||
|
if (!config.value?.fontStrokeEnabled) return ''
|
||||||
|
const w = config.value?.fontStrokeWidth ?? 1
|
||||||
|
const c = config.value?.fontStrokeColor ?? '#000000'
|
||||||
|
// 8 方向阴影构成描边
|
||||||
|
const s = `${w}px 0 0 ${c}, -${w}px 0 0 ${c}, 0 ${w}px 0 ${c}, 0 -${w}px 0 ${c}, ${w}px ${w}px 0 ${c}, -${w}px -${w}px 0 ${c}, ${w}px -${w}px 0 ${c}, -${w}px ${w}px 0 ${c}`
|
||||||
|
return `text-shadow: ${s}`
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 分组后的显示项(group / multiline 布局用) */
|
||||||
|
const osdGroups = computed<OsdGroup[]>(() => {
|
||||||
|
if (!config.value?.overlayItems?.length) return []
|
||||||
|
return groupOsdItems(config.value.overlayItems)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== 窗口拖动(右键长按触发,由 Rust 后端检测并发事件) =====
|
||||||
|
async function startNativeDrag() {
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().startDragging()
|
||||||
|
} catch {
|
||||||
|
/* 忽略 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 实际内容尺寸测量与上报(替代不准确的估算) =====
|
||||||
|
const osdRootEl = ref<HTMLElement | null>(null)
|
||||||
|
let measureTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
/** 测量 osd-bar 实际渲染尺寸,上报给主窗口调整窗口大小 */
|
||||||
|
async function measureAndReportSize() {
|
||||||
|
await nextTick()
|
||||||
|
const root = osdRootEl.value
|
||||||
|
if (!root) return
|
||||||
|
const bar = root.querySelector<HTMLElement>('.osd-bar')
|
||||||
|
if (!bar) return
|
||||||
|
// 用 getBoundingClientRect 取实际尺寸(逻辑像素)
|
||||||
|
const rect = bar.getBoundingClientRect()
|
||||||
|
if (rect.width === 0 || rect.height === 0) return
|
||||||
|
// 额外留 1px 余量避免边缘裁切
|
||||||
|
await emit('osd-content-size', { width: Math.ceil(rect.width) + 1, height: Math.ceil(rect.height) + 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 防抖测量(数据频繁更新时合并) */
|
||||||
|
function scheduleMeasure() {
|
||||||
|
if (measureTimer) clearTimeout(measureTimer)
|
||||||
|
measureTimer = setTimeout(() => {
|
||||||
|
measureTimer = null
|
||||||
|
void measureAndReportSize()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 应用鼠标穿透 =====
|
||||||
|
// 同时调用 Tauri setIgnoreCursorEvents(处理 webview2 子窗口)和 Rust WS_EX_TRANSPARENT(处理原生窗口)
|
||||||
|
// 仅靠原生 WS_EX_TRANSPARENT 不足:Tauri 窗口包含 webview2 子窗口,需两者都设置才能完全穿透
|
||||||
|
async function applyClickThrough(ignore: boolean) {
|
||||||
|
// 1. Tauri API:处理 webview2 层的鼠标事件穿透
|
||||||
|
try {
|
||||||
|
await getCurrentWindow().setIgnoreCursorEvents(ignore)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OSD] setIgnoreCursorEvents 失败:', e)
|
||||||
|
}
|
||||||
|
// 2. Rust 原生:设置 WS_EX_TRANSPARENT 扩展样式(更可靠的原生层穿透)
|
||||||
|
try {
|
||||||
|
await invoke('osd_set_click_through', { label: 'osd-overlay', enabled: ignore })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OSD] osd_set_click_through 失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 应用置顶(使用 Rust 原生命令) =====
|
||||||
|
async function applyTopmost(topmost: boolean) {
|
||||||
|
try {
|
||||||
|
await invoke('osd_set_topmost', { label: 'osd-overlay', topmost })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OSD] 设置置顶失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听配置变化:应用点击穿透
|
||||||
|
watch(() => config.value?.clickThrough, (ignore) => {
|
||||||
|
if (ignore === undefined) return
|
||||||
|
applyClickThrough(Boolean(ignore))
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 应用原生样式(NoActivate + ToolWindow,不获取焦点)
|
||||||
|
try {
|
||||||
|
await invoke('osd_apply_overlay_style', { label: 'osd-overlay' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OSD] 应用原生样式失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认置顶
|
||||||
|
await applyTopmost(true)
|
||||||
|
|
||||||
|
// 启动任务栏覆盖监视(系统 UI 出现时暂时取消置顶)
|
||||||
|
try {
|
||||||
|
await invoke('osd_start_topmost_watch')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[OSD] 启动置顶监视失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听主窗口推送的 OSD 状态
|
||||||
|
unlistenFns.push(await listen<OsdStatePayload>('osd-state-update', (e) => {
|
||||||
|
config.value = e.payload.config
|
||||||
|
snapshot.value = e.payload.snapshot
|
||||||
|
networkSpeed.value = e.payload.networkSpeed
|
||||||
|
// 数据/配置变化后重新测量尺寸
|
||||||
|
scheduleMeasure()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 监听系统 UI 覆盖事件
|
||||||
|
unlistenFns.push(await listen('osd-system-ui-active', async () => {
|
||||||
|
await applyTopmost(false)
|
||||||
|
}))
|
||||||
|
|
||||||
|
unlistenFns.push(await listen('osd-system-ui-inactive', async () => {
|
||||||
|
await applyTopmost(true)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 鼠标按下:仅在关闭穿透时响应左键拖动 */
|
||||||
|
async function onMouseDown(e: MouseEvent) {
|
||||||
|
if (e.button !== 0) return // 仅左键
|
||||||
|
if (config.value?.clickThrough) return // 穿透模式下不响应
|
||||||
|
e.preventDefault()
|
||||||
|
// 确保穿透已关闭(双保险:防止穿透状态不同步导致 startDragging 无效)
|
||||||
|
await applyClickThrough(false)
|
||||||
|
// 使用 Tauri 官方 startDragging API(比 SendMessageW(WM_NCLBUTTONDOWN) 从其他线程调用更可靠)
|
||||||
|
await startNativeDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unlistenFns.forEach(fn => fn())
|
||||||
|
// 停止监视线程
|
||||||
|
invoke('osd_stop_watch').catch(() => {})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="config"
|
||||||
|
ref="osdRootEl"
|
||||||
|
class="osd-root select-none"
|
||||||
|
:style="`font-size: ${config.fontSize}px; ${fontStrokeStyle}`"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
>
|
||||||
|
<!-- ===== 单行布局:分组式,组间用 | 分隔,固定宽度 ===== -->
|
||||||
|
<div
|
||||||
|
v-if="config.overlayItems.length && config.layout === 'single'"
|
||||||
|
class="osd-bar osd-bar-single"
|
||||||
|
:style="{ background: config.bgColor }"
|
||||||
|
>
|
||||||
|
<template v-for="(g, gi) in osdGroups" :key="g.key">
|
||||||
|
<span v-if="gi > 0" class="osd-sg-sep" :style="{ color: fontColor }">|</span>
|
||||||
|
<span class="osd-sg-group">
|
||||||
|
<span v-if="config.showLabel" class="osd-sg-label" :style="{ color: groupLabelColor(g) }">{{ g.label }}</span>
|
||||||
|
<span
|
||||||
|
v-for="item in g.items"
|
||||||
|
:key="item.key"
|
||||||
|
class="osd-sg-item"
|
||||||
|
:style="{ color: itemColorWithAlert(item) }"
|
||||||
|
>
|
||||||
|
<span v-if="netArrow(item)" class="osd-net-arrow">{{ netArrow(item) }}</span>
|
||||||
|
<span class="osd-fixed-num">{{ fmtFixedValue(getOsdItemValue(item), item) }}</span>
|
||||||
|
<span class="osd-fixed-unit">{{ fmtFixedUnit(item) }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 分组横排布局:每组标题在上 + 数据列在下,固定宽度 ===== -->
|
||||||
|
<div
|
||||||
|
v-else-if="config.overlayItems.length && config.layout === 'group'"
|
||||||
|
class="osd-bar osd-bar-group"
|
||||||
|
:style="{ background: config.bgColor }"
|
||||||
|
>
|
||||||
|
<div v-for="g in osdGroups" :key="g.key" class="osd-group">
|
||||||
|
<div class="osd-group-header" :style="{ color: groupLabelColor(g) }">{{ g.label }}</div>
|
||||||
|
<div class="osd-group-data">
|
||||||
|
<span
|
||||||
|
v-for="item in g.items"
|
||||||
|
:key="item.key"
|
||||||
|
class="osd-group-item"
|
||||||
|
:style="{ color: itemColorWithAlert(item) }"
|
||||||
|
>
|
||||||
|
<span v-if="netArrow(item)" class="osd-net-arrow">{{ netArrow(item) }}</span>
|
||||||
|
<span class="osd-fixed-num">{{ fmtFixedValue(getOsdItemValue(item), item) }}</span>
|
||||||
|
<span class="osd-fixed-unit">{{ fmtFixedUnit(item) }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== 多行布局:每组一行,左对齐,类小飞机 ===== -->
|
||||||
|
<div
|
||||||
|
v-else-if="config.overlayItems.length && config.layout === 'multiline'"
|
||||||
|
class="osd-bar osd-bar-multiline"
|
||||||
|
:style="{ background: config.bgColor }"
|
||||||
|
>
|
||||||
|
<div v-for="g in osdGroups" :key="g.key" class="osd-line">
|
||||||
|
<span class="osd-line-label" :style="{ color: groupLabelColor(g) }">{{ g.label }}</span>
|
||||||
|
<span
|
||||||
|
v-for="item in g.items"
|
||||||
|
:key="item.key"
|
||||||
|
class="osd-line-item"
|
||||||
|
:style="{ color: itemColorWithAlert(item) }"
|
||||||
|
>
|
||||||
|
<span v-if="netArrow(item)" class="osd-net-arrow">{{ netArrow(item) }}</span>
|
||||||
|
<span class="osd-fixed-num">{{ fmtFixedValue(getOsdItemValue(item), item) }}</span>
|
||||||
|
<span class="osd-fixed-unit">{{ fmtFixedUnit(item) }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无显示项占位 -->
|
||||||
|
<div v-else class="osd-empty">未配置显示项</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 根容器:填满窗口 */
|
||||||
|
.osd-root {
|
||||||
|
font-family: 'Cascadia Code', 'Consolas', 'Microsoft YaHei', monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
background: transparent;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OSD 容器:inline-flex 让宽度自适应内容,每项紧凑排列 */
|
||||||
|
.osd-bar {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: stretch;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
padding: 3px 4px;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 单行布局:分组式,组间用 | 分隔 ===== */
|
||||||
|
.osd-bar-single {
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-sg-sep {
|
||||||
|
opacity: 0.4;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-sg-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-sg-label {
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-sg-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 分组横排布局:每组标题在上 + 数据列在下 ===== */
|
||||||
|
.osd-bar-group {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-group-header {
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
padding-bottom: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-group-data {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-group-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 多行布局:每组一行,左对齐,类小飞机 ===== */
|
||||||
|
.osd-bar-multiline {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-line-label {
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.7;
|
||||||
|
min-width: 4ch;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-line-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 固定宽度文本(group / multiline 布局用,white-space: pre 保留 padNum 补的空格) ===== */
|
||||||
|
.osd-fixed-num {
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-fixed-unit {
|
||||||
|
opacity: 0.7;
|
||||||
|
white-space: pre;
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-net-arrow {
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osd-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+19
-28
@@ -44,7 +44,7 @@ export interface MonitorStatus {
|
|||||||
restartCount: number
|
restartCount: number
|
||||||
/** 是否为提权模式(通过 UAC 以管理员权限启动) */
|
/** 是否为提权模式(通过 UAC 以管理员权限启动) */
|
||||||
elevated: boolean
|
elevated: boolean
|
||||||
/** Thing 自身是否以管理员权限运行(永久提权模式) */
|
/** Thing 自身是否以管理员权限运行(提权模式) */
|
||||||
thingElevated: boolean
|
thingElevated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
/** 是否已完成首次加载(避免初始 null/false 导致 UI 闪烁误导状态) */
|
/** 是否已完成首次加载(避免初始 null/false 导致 UI 闪烁误导状态) */
|
||||||
const initialized = ref(false)
|
const initialized = ref(false)
|
||||||
|
|
||||||
/** 永久提权标志是否已启用(后续启动自动触发 UAC) */
|
/** 提权标志是否已启用(后续启动自动触发 UAC) */
|
||||||
const elevateOnLaunch = ref(false)
|
const elevateOnLaunch = ref(false)
|
||||||
|
|
||||||
/** 硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
|
/** 硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
|
||||||
@@ -138,8 +138,8 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
if (!status.value) return 'idle'
|
if (!status.value) return 'idle'
|
||||||
if (!status.value.running) return 'idle'
|
if (!status.value.running) return 'idle'
|
||||||
if (!status.value.ready) return 'loading'
|
if (!status.value.ready) return 'loading'
|
||||||
if (eventCount.value === 0) return 'loading'
|
// Kernel 已 ready 即视为已连接(避免 SSE 首事件延迟导致一直显示"启动中")
|
||||||
if (Date.now() - lastEventTime.value > STALE_TIMEOUT_MS) return 'disconnected'
|
if (Date.now() - lastEventTime.value > STALE_TIMEOUT_MS && eventCount.value > 0) return 'disconnected'
|
||||||
return 'connected'
|
return 'connected'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -197,6 +197,14 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
try {
|
try {
|
||||||
await invoke('monitor_start')
|
await invoke('monitor_start')
|
||||||
await refreshStatus()
|
await refreshStatus()
|
||||||
|
// 进程拉起可能存在竞态(spawn 后状态未立即更新),短暂重试确保 running=true
|
||||||
|
if (!status.value?.running) {
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 500))
|
||||||
|
await refreshStatus()
|
||||||
|
if (status.value?.running) break
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg.value = String(e)
|
errorMsg.value = String(e)
|
||||||
logger.error('启动失败: ' + e)
|
logger.error('启动失败: ' + e)
|
||||||
@@ -222,26 +230,8 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 以管理员权限重启 Kernel(弹 UAC)。
|
/** 提权:以管理员权限重启 Thing 自身,ThingHK 子进程会继承管理员权限。
|
||||||
* 停止当前 Kernel → ShellExecute "runas" → 等待 ready → 重新订阅 SSE。
|
* Thing 以管理员权限运行时,ProcessManager 可直接管控 ThingHK,崩溃自动重启。
|
||||||
* 提权后进程不归 ProcessManager 管,停止走 /shutdown 接口。
|
|
||||||
*/
|
|
||||||
async function startElevated() {
|
|
||||||
if (starting.value) return
|
|
||||||
starting.value = true
|
|
||||||
errorMsg.value = null
|
|
||||||
try {
|
|
||||||
await invoke('monitor_start_elevated')
|
|
||||||
await refreshStatus()
|
|
||||||
} catch (e) {
|
|
||||||
errorMsg.value = String(e)
|
|
||||||
logger.error('提权启动失败: ' + e)
|
|
||||||
} finally {
|
|
||||||
starting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 永久提权:以管理员权限重启 Thing 自身,ThingHK 子进程会继承管理员权限。
|
|
||||||
* 非管理员时进程退出;已是管理员时仅设置标志并返回。 */
|
* 非管理员时进程退出;已是管理员时仅设置标志并返回。 */
|
||||||
async function elevateSelf() {
|
async function elevateSelf() {
|
||||||
if (starting.value) return
|
if (starting.value) return
|
||||||
@@ -253,13 +243,13 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
await refreshElevateOnLaunch()
|
await refreshElevateOnLaunch()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg.value = String(e)
|
errorMsg.value = String(e)
|
||||||
logger.error('永久提权失败: ' + e)
|
logger.error('提权失败: ' + e)
|
||||||
} finally {
|
} finally {
|
||||||
starting.value = false
|
starting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 刷新永久提权标志状态 */
|
/** 刷新提权标志状态 */
|
||||||
async function refreshElevateOnLaunch() {
|
async function refreshElevateOnLaunch() {
|
||||||
try {
|
try {
|
||||||
elevateOnLaunch.value = await invoke<boolean>('monitor_get_elevate_on_launch')
|
elevateOnLaunch.value = await invoke<boolean>('monitor_get_elevate_on_launch')
|
||||||
@@ -269,7 +259,7 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
return elevateOnLaunch.value
|
return elevateOnLaunch.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取消永久提权:清除标志,下次启动不再触发 UAC(当前会话权限不变) */
|
/** 取消提权:清除标志,下次启动不再触发 UAC(当前会话权限不变) */
|
||||||
async function cancelElevation() {
|
async function cancelElevation() {
|
||||||
try {
|
try {
|
||||||
await invoke('monitor_set_elevate_on_launch', { enabled: false })
|
await invoke('monitor_set_elevate_on_launch', { enabled: false })
|
||||||
@@ -337,6 +327,8 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
}))
|
}))
|
||||||
unlistenFns.push(await listen('monitor-ready', () => {
|
unlistenFns.push(await listen('monitor-ready', () => {
|
||||||
refreshStatus()
|
refreshStatus()
|
||||||
|
// Kernel 就绪后主动拉取一次快照,避免等待 SSE 首事件导致 UI 空白
|
||||||
|
fetchSnapshot()
|
||||||
}))
|
}))
|
||||||
unlistenFns.push(await listen('monitor-loading', () => {
|
unlistenFns.push(await listen('monitor-loading', () => {
|
||||||
// 后端正在等待 Kernel ready,刷新状态以反映 running=true
|
// 后端正在等待 Kernel ready,刷新状态以反映 running=true
|
||||||
@@ -416,7 +408,6 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
refreshKernelInfo,
|
refreshKernelInfo,
|
||||||
refreshElevateOnLaunch,
|
refreshElevateOnLaunch,
|
||||||
start,
|
start,
|
||||||
startElevated,
|
|
||||||
elevateSelf,
|
elevateSelf,
|
||||||
cancelElevation,
|
cancelElevation,
|
||||||
fetchHardwareConfig,
|
fetchHardwareConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user